Compare commits
1 Commits
main
...
jp-static-
Author | SHA1 | Date |
---|---|---|
![]() |
c261a863fb |
|
@ -1 +0,0 @@
|
||||||
.build
|
|
15
.bazelrc
15
.bazelrc
|
@ -1,15 +0,0 @@
|
||||||
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 \
|
|
||||||
--features=swift.opt_uses_wmo
|
|
|
@ -1 +0,0 @@
|
||||||
6.2.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": ""
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
steps:
|
|
||||||
- label: "Bazel"
|
|
||||||
commands:
|
|
||||||
- echo "+++ Build"
|
|
||||||
- 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 "+++ Run Danger"
|
|
||||||
- bundle exec danger --verbose
|
|
||||||
- label: "TSan Tests"
|
|
||||||
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
|
|
|
@ -1,5 +1,4 @@
|
||||||
*
|
*
|
||||||
!Plugins
|
|
||||||
!Source
|
!Source
|
||||||
!Tests
|
!Tests
|
||||||
!Package.*
|
!Package.*
|
||||||
|
|
|
@ -3,16 +3,16 @@ name: docker
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- master
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Extract DOCKER_TAG using tag name
|
- name: Extract DOCKER_TAG using tag name
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# Xcode
|
# Xcode
|
||||||
#
|
#
|
||||||
|
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||||
|
|
||||||
## Build generated
|
## Build generated
|
||||||
build/
|
build/
|
||||||
|
@ -21,7 +22,6 @@ xcuserdata
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
*.xcscmblueprint
|
*.xcscmblueprint
|
||||||
default.profraw
|
|
||||||
|
|
||||||
## Obj-C/Swift specific
|
## Obj-C/Swift specific
|
||||||
*.hmap
|
*.hmap
|
||||||
|
@ -37,17 +37,14 @@ default.profraw
|
||||||
|
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
SwiftLint.xcodeproj
|
|
||||||
SwiftLint.pkg
|
SwiftLint.pkg
|
||||||
*.zip
|
SwiftLintFramework.framework.zip
|
||||||
benchmark_*
|
benchmark_*
|
||||||
|
portable_swiftlint.zip
|
||||||
|
swiftlint_linux.zip
|
||||||
osscheck/
|
osscheck/
|
||||||
docs/
|
docs/
|
||||||
rule_docs/
|
rule_docs/
|
||||||
bazel.tar.gz
|
|
||||||
bazel.tar.gz.sha256
|
|
||||||
ci.bazelrc
|
|
||||||
user.bazelrc
|
|
||||||
|
|
||||||
# Swift Package Manager
|
# Swift Package Manager
|
||||||
#
|
#
|
||||||
|
@ -62,6 +59,3 @@ Packages/
|
||||||
# Bundler
|
# Bundler
|
||||||
.bundle/
|
.bundle/
|
||||||
bundle/
|
bundle/
|
||||||
|
|
||||||
# Bazel
|
|
||||||
bazel-*
|
|
||||||
|
|
11
.jazzy.yaml
11
.jazzy.yaml
|
@ -1,20 +1,19 @@
|
||||||
module: SwiftLintCore
|
module: SwiftLintFramework
|
||||||
author: JP Simard, SwiftLint Contributors
|
author: JP Simard, SwiftLint Contributors
|
||||||
author_url: https://jpsim.com
|
author_url: https://jpsim.com
|
||||||
root_url: https://realm.github.io/SwiftLint/
|
root_url: https://realm.github.io/SwiftLint/
|
||||||
github_url: https://github.com/realm/SwiftLint
|
github_url: https://github.com/realm/SwiftLint
|
||||||
github_file_prefix: https://github.com/realm/SwiftLint/tree/main
|
github_file_prefix: https://github.com/realm/SwiftLint/tree/master
|
||||||
swift_build_tool: spm
|
swift_build_tool: spm
|
||||||
theme: fullwidth
|
theme: fullwidth
|
||||||
clean: true
|
clean: true
|
||||||
copyright: '© 2023 [JP Simard](https://jpsim.com) under MIT.'
|
copyright: '© 2020 [JP Simard](https://jpsim.com) under MIT.'
|
||||||
|
|
||||||
documentation: rule_docs/*.md
|
documentation: rule_docs/*.md
|
||||||
hide_unlisted_documentation: true
|
hide_unlisted_documentation: true
|
||||||
custom_categories_unlisted_prefix: ''
|
custom_categories_unlisted_prefix: ''
|
||||||
exclude:
|
exclude:
|
||||||
# TODO: Document extensions
|
- Source/SwiftLintFramework/Rules/**/*.swift
|
||||||
- Source/SwiftLintCore/Extensions/*.swift
|
|
||||||
custom_categories:
|
custom_categories:
|
||||||
- name: Rules
|
- name: Rules
|
||||||
children:
|
children:
|
||||||
|
@ -23,7 +22,7 @@ custom_categories:
|
||||||
children:
|
children:
|
||||||
- CSVReporter
|
- CSVReporter
|
||||||
- CheckstyleReporter
|
- CheckstyleReporter
|
||||||
- CodeClimateReporter
|
- CodeclimateReporter
|
||||||
- EmojiReporter
|
- EmojiReporter
|
||||||
- GitHubActionsLoggingReporter
|
- GitHubActionsLoggingReporter
|
||||||
- HTMLReporter
|
- HTMLReporter
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
import SwiftLintFramework
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
// swiftlint:disable file_length single_test_class type_name
|
||||||
|
|
||||||
|
{% for rule in types.structs|based:"AutomaticTestableRule" %}
|
||||||
|
class {{ rule.name }}Tests: XCTestCase {
|
||||||
|
func testWithDefaultConfiguration() {
|
||||||
|
verifyRule({{ rule.name }}.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% if not forloop.last %}
|
||||||
|
|
||||||
|
{% endif %}{% endfor %}
|
|
@ -1,20 +0,0 @@
|
||||||
@testable import SwiftLintBuiltInRules
|
|
||||||
@_spi(TestHelper)
|
|
||||||
@testable import SwiftLintCore
|
|
||||||
import SwiftLintTestHelpers
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
func testWithDefaultConfiguration() {
|
|
||||||
verifyRule({{ rule.name }}.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{% if not forloop.last %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
@testable import SwiftLintFrameworkTests
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
// swiftlint:disable line_length file_length
|
||||||
|
|
||||||
|
{% for type in types.classes|based:"XCTestCase" %}
|
||||||
|
extension {{ type.name }} {
|
||||||
|
static var allTests: [(String, ({{ type.name }}) -> () throws -> Void)] = [
|
||||||
|
{% for method in type.methods where method.parameters.count == 0 and method.shortName|hasPrefix:"test" and method|!annotated:"skipTestOnLinux" %} ("{{ method.shortName }}", {{ method.shortName }}){% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}]
|
||||||
|
}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
XCTMain([
|
||||||
|
{% for type in types.classes|based:"XCTestCase" %} testCase({{ type.name }}.allTests){% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}])
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
/// The rule list containing all available rules built into SwiftLint.
|
/// The rule list containing all available rules built into SwiftLint.
|
||||||
public let builtInRules: [Rule.Type] = [
|
public let primaryRuleList = RuleList(rules: [
|
||||||
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
|
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
|
||||||
{% endfor %}]
|
{% endfor %}])
|
|
@ -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 %}
|
|
||||||
]
|
|
141
.swiftlint.yml
141
.swiftlint.yml
|
@ -1,5 +1,4 @@
|
||||||
included:
|
included:
|
||||||
- Plugins
|
|
||||||
- Source
|
- Source
|
||||||
- Tests
|
- Tests
|
||||||
excluded:
|
excluded:
|
||||||
|
@ -8,84 +7,95 @@ analyzer_rules:
|
||||||
- unused_declaration
|
- unused_declaration
|
||||||
- unused_import
|
- unused_import
|
||||||
opt_in_rules:
|
opt_in_rules:
|
||||||
- all
|
|
||||||
disabled_rules:
|
|
||||||
- anonymous_argument_in_multiline_closure
|
|
||||||
- anyobject_protocol
|
- anyobject_protocol
|
||||||
- closure_body_length
|
- array_init
|
||||||
- conditional_returns_on_newline
|
- attributes
|
||||||
- convenience_type
|
- closure_end_indentation
|
||||||
- discouraged_optional_collection
|
- closure_spacing
|
||||||
- explicit_acl
|
- collection_alignment
|
||||||
- explicit_enum_raw_value
|
- contains_over_filter_count
|
||||||
- explicit_top_level_acl
|
- contains_over_filter_is_empty
|
||||||
- explicit_type_interface
|
- contains_over_first_not_nil
|
||||||
- file_types_order
|
- contains_over_range_nil_comparison
|
||||||
- force_unwrapping
|
- discouraged_none_name
|
||||||
- function_default_parameter_at_end
|
- discouraged_object_literal
|
||||||
- implicit_return
|
- empty_collection_literal
|
||||||
- implicitly_unwrapped_optional
|
- empty_count
|
||||||
- indentation_width
|
- empty_string
|
||||||
- inert_defer
|
- empty_xctest_method
|
||||||
- missing_docs
|
- enum_case_associated_values_count
|
||||||
- multiline_arguments
|
- explicit_init
|
||||||
- multiline_arguments_brackets
|
- extension_access_modifier
|
||||||
- multiline_function_chains
|
- fallthrough
|
||||||
- multiline_literal_brackets
|
- fatal_error_message
|
||||||
- multiline_parameters
|
- file_header
|
||||||
- multiline_parameters_brackets
|
- file_name
|
||||||
- no_extension_access_modifier
|
- first_where
|
||||||
- no_fallthrough_only
|
- flatmap_over_map_reduce
|
||||||
- no_grouping_extension
|
- identical_operands
|
||||||
- no_magic_numbers
|
- joined_default_parameter
|
||||||
- prefer_nimble
|
- last_where
|
||||||
|
- legacy_multiple
|
||||||
|
- legacy_random
|
||||||
|
- 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
|
- prefer_self_in_static_references
|
||||||
- prefixed_toplevel_constant
|
- prefer_self_type_over_type_of_self
|
||||||
- redundant_self_in_closure
|
- private_action
|
||||||
- required_deinit
|
- private_outlet
|
||||||
- self_binding
|
- prohibited_interface_builder
|
||||||
- sorted_enum_cases
|
- prohibited_super_call
|
||||||
- strict_fileprivate
|
- quick_discouraged_call
|
||||||
- superfluous_else
|
- quick_discouraged_focused_test
|
||||||
- switch_case_on_newline
|
- quick_discouraged_pending_test
|
||||||
- todo
|
- reduce_into
|
||||||
- trailing_closure
|
- redundant_nil_coalescing
|
||||||
- type_contents_order
|
- redundant_type_annotation
|
||||||
- unused_capture_list
|
- single_test_class
|
||||||
- vertical_whitespace_between_cases
|
- sorted_first_last
|
||||||
|
- sorted_imports
|
||||||
|
- static_operator
|
||||||
|
- strong_iboutlet
|
||||||
|
- test_case_accessibility
|
||||||
|
- toggle_bool
|
||||||
|
- unavailable_function
|
||||||
|
- unneeded_parentheses_in_closure_argument
|
||||||
|
- unowned_variable_capture
|
||||||
|
- untyped_error_in_catch
|
||||||
|
- vertical_parameter_alignment_on_call
|
||||||
|
- vertical_whitespace_closing_braces
|
||||||
|
- vertical_whitespace_opening_braces
|
||||||
|
- xct_specific_matcher
|
||||||
|
- yoda_condition
|
||||||
|
|
||||||
attributes:
|
|
||||||
always_on_line_above:
|
|
||||||
- "@OptionGroup"
|
|
||||||
identifier_name:
|
identifier_name:
|
||||||
excluded:
|
excluded:
|
||||||
- id
|
- id
|
||||||
large_tuple: 3
|
|
||||||
number_separator:
|
number_separator:
|
||||||
minimum_length: 5
|
minimum_length: 5
|
||||||
file_name:
|
file_name:
|
||||||
excluded:
|
excluded:
|
||||||
- Exports.swift
|
- main.swift
|
||||||
- GeneratedTests.swift
|
- LinuxMain.swift
|
||||||
- SwiftSyntax+SwiftLint.swift
|
|
||||||
- TestHelpers.swift
|
- TestHelpers.swift
|
||||||
|
- shim.swift
|
||||||
balanced_xctest_lifecycle: &unit_test_configuration
|
- AutomaticRuleTests.generated.swift
|
||||||
test_parent_classes:
|
|
||||||
- SwiftLintTestCase
|
|
||||||
- XCTestCase
|
|
||||||
empty_xctest_method: *unit_test_configuration
|
|
||||||
single_test_class: *unit_test_configuration
|
|
||||||
|
|
||||||
function_body_length: 60
|
|
||||||
type_body_length: 400
|
|
||||||
|
|
||||||
custom_rules:
|
custom_rules:
|
||||||
rule_id:
|
rule_id:
|
||||||
included: Source/SwiftLintBuiltInRules/Rules/.+/\w+\.swift
|
included: Source/SwiftLintFramework/Rules/.+/\w+\.swift
|
||||||
name: Rule ID
|
name: Rule ID
|
||||||
message: Rule IDs must be all lowercase, snake case and not end with `rule`
|
message: Rule IDs must be all lowercase, snake case and not end with `rule`
|
||||||
regex: ^\s+identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
|
regex: identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
|
||||||
severity: error
|
severity: error
|
||||||
fatal_error:
|
fatal_error:
|
||||||
name: Fatal Error
|
name: Fatal Error
|
||||||
|
@ -101,8 +111,3 @@ custom_rules:
|
||||||
message: Rule Test Function mustn't end with `rule`
|
message: Rule Test Function mustn't end with `rule`
|
||||||
regex: func\s*test\w+(r|R)ule\(\)
|
regex: func\s*test\w+(r|R)ule\(\)
|
||||||
severity: error
|
severity: error
|
||||||
|
|
||||||
unused_import:
|
|
||||||
always_keep_imports:
|
|
||||||
- SwiftSyntaxBuilder # we can't detect uses of string interpolation of swift syntax nodes
|
|
||||||
- SwiftLintFramework # now that this is a wrapper around other modules, don't treat as unused
|
|
||||||
|
|
211
BUILD
211
BUILD
|
@ -1,211 +0,0 @@
|
||||||
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",
|
|
||||||
"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"],
|
|
||||||
),
|
|
||||||
module_name = "SwiftLintFramework",
|
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
deps = [
|
|
||||||
":SwiftLintBuiltInRules",
|
|
||||||
":SwiftLintCore",
|
|
||||||
":SwiftLintExtraRules",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
swift_library(
|
|
||||||
name = "swiftlint.library",
|
|
||||||
srcs = glob(["Source/swiftlint/**/*.swift"]),
|
|
||||||
module_name = "swiftlint",
|
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
deps = [
|
|
||||||
":SwiftLintFramework",
|
|
||||||
"@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit",
|
|
||||||
"@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser",
|
|
||||||
"@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
swift_binary(
|
|
||||||
name = "swiftlint",
|
|
||||||
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(
|
|
||||||
name = "LintInputs",
|
|
||||||
srcs = glob(["Source/**/*.swift"]) + [
|
|
||||||
".swiftlint.yml",
|
|
||||||
"//Tests:SwiftLintFrameworkTestsData",
|
|
||||||
],
|
|
||||||
visibility = ["//Tests:__subpackages__"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Release
|
|
||||||
|
|
||||||
filegroup(
|
|
||||||
name = "release_files",
|
|
||||||
srcs = [
|
|
||||||
"BUILD",
|
|
||||||
"LICENSE",
|
|
||||||
"MODULE.bazel",
|
|
||||||
"//:DyldWarningWorkaroundSources",
|
|
||||||
"//:LintInputs",
|
|
||||||
"//Tests:BUILD",
|
|
||||||
"//bazel:release_files",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: Use rules_pkg
|
|
||||||
genrule(
|
|
||||||
name = "release",
|
|
||||||
srcs = [":release_files"],
|
|
||||||
outs = [
|
|
||||||
"bazel.tar.gz",
|
|
||||||
"bazel.tar.gz.sha256",
|
|
||||||
],
|
|
||||||
cmd = """\
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
outs=($(OUTS))
|
|
||||||
|
|
||||||
COPYFILE_DISABLE=1 tar czvfh "$${outs[0]}" \
|
|
||||||
--exclude ^bazel-out/ \
|
|
||||||
--exclude ^external/ \
|
|
||||||
*
|
|
||||||
shasum -a 256 "$${outs[0]}" > "$${outs[1]}"
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Xcode Integration
|
|
||||||
|
|
||||||
xcodeproj(
|
|
||||||
name = "xcodeproj",
|
|
||||||
project_name = "SwiftLint",
|
|
||||||
schemes = [
|
|
||||||
xcode_schemes.scheme(
|
|
||||||
name = "SwiftLint",
|
|
||||||
launch_action = xcode_schemes.launch_action(
|
|
||||||
"swiftlint",
|
|
||||||
args = [
|
|
||||||
"--progress",
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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
|
|
||||||
|
|
||||||
sh_test(
|
|
||||||
name = "analyze",
|
|
||||||
srcs = ["//tools:test-analyze.sh"],
|
|
||||||
data = [
|
|
||||||
"Package.resolved",
|
|
||||||
"Package.swift",
|
|
||||||
":LintInputs",
|
|
||||||
":swiftlint",
|
|
||||||
],
|
|
||||||
)
|
|
1404
CHANGELOG.md
1404
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,8 @@
|
||||||
## Tutorial
|
|
||||||
|
|
||||||
If you'd like to write a SwiftLint rule but aren't sure how to start,
|
|
||||||
please watch and follow along with
|
|
||||||
[this video tutorial](https://vimeo.com/819268038).
|
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
|
|
||||||
All changes, no matter how trivial, must be done via pull request. Commits
|
All changes, no matter how trivial, must be done via pull request. Commits
|
||||||
should never be made directly on the `main` branch. Prefer rebasing over
|
should never be made directly on the `master` branch. Prefer rebasing over
|
||||||
merging `main` into your PR branch to update it and resolve conflicts.
|
merging `master` into your PR branch to update it and resolve conflicts.
|
||||||
|
|
||||||
_If you have commit access to SwiftLint and believe your change to be trivial
|
_If you have commit access to SwiftLint and believe your change to be trivial
|
||||||
and not worth waiting for review, you may open a pull request and merge
|
and not worth waiting for review, you may open a pull request and merge
|
||||||
|
@ -64,7 +58,7 @@ $ make docker_test
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory.
|
New rules should be added in the `Source/SwiftLintFramework/Rules` directory.
|
||||||
|
|
||||||
Rules should conform to either the `Rule` or `ASTRule` protocols.
|
Rules should conform to either the `Rule` or `ASTRule` protocols.
|
||||||
|
|
||||||
|
@ -78,19 +72,6 @@ over time. This way adding a unit test for your new Rule is just a matter of
|
||||||
adding a test case in `RulesTests.swift` which simply calls
|
adding a test case in `RulesTests.swift` which simply calls
|
||||||
`verifyRule(YourNewRule.description)`.
|
`verifyRule(YourNewRule.description)`.
|
||||||
|
|
||||||
For debugging purposes examples can be marked as `focused`. If there are any
|
|
||||||
focused examples found, then only those will be run when running tests for that rule.
|
|
||||||
```
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("let x: [Int]"),
|
|
||||||
Example("let x: [Int: String]").focused() // only this one will be run in tests
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("let x: ↓Array<String>"),
|
|
||||||
Example("let x: ↓Dictionary<Int, String>")
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ConfigurationProviderRule`
|
### `ConfigurationProviderRule`
|
||||||
|
|
||||||
If your rule supports user-configurable options via `.swiftlint.yml`, you can
|
If your rule supports user-configurable options via `.swiftlint.yml`, you can
|
||||||
|
@ -104,11 +85,11 @@ configuration object via the `configuration` property:
|
||||||
* If none of the provided `RuleConfiguration`s are applicable, you can create one
|
* If none of the provided `RuleConfiguration`s are applicable, you can create one
|
||||||
specifically for your rule.
|
specifically for your rule.
|
||||||
|
|
||||||
See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceCastRule.swift)
|
See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/Idiomatic/ForceCastRule.swift)
|
||||||
for a rule that allows severity configuration,
|
for a rule that allows severity configuration,
|
||||||
[`FileLengthRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift)
|
[`FileLengthRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/Metrics/FileLengthRule.swift)
|
||||||
for a rule that has multiple severity levels,
|
for a rule that has multiple severity levels,
|
||||||
[`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift)
|
[`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/Style/IdentifierNameRule.swift)
|
||||||
for a rule that allows name evaluation configuration:
|
for a rule that allows name evaluation configuration:
|
||||||
|
|
||||||
``` yaml
|
``` yaml
|
||||||
|
@ -145,8 +126,8 @@ If your rule is configurable, but does not fit the pattern of
|
||||||
|
|
||||||
All changes should be made via pull requests on GitHub.
|
All changes should be made via pull requests on GitHub.
|
||||||
|
|
||||||
When issuing a pull request with user-facing changes, please add a
|
When issuing a pull request, please add a summary of your changes to
|
||||||
summary of your changes to the `CHANGELOG.md` file.
|
the `CHANGELOG.md` file.
|
||||||
|
|
||||||
We follow the same syntax as CocoaPods' CHANGELOG.md:
|
We follow the same syntax as CocoaPods' CHANGELOG.md:
|
||||||
|
|
||||||
|
@ -158,27 +139,3 @@ We follow the same syntax as CocoaPods' CHANGELOG.md:
|
||||||
per line. Usually just one. If there was no issue tracking this change,
|
per line. Usually just one. If there was no issue tracking this change,
|
||||||
you may instead link to the change's pull request.
|
you may instead link to the change's pull request.
|
||||||
5. All CHANGELOG.md content is hard-wrapped at 80 characters.
|
5. All CHANGELOG.md content is hard-wrapped at 80 characters.
|
||||||
|
|
||||||
## CI
|
|
||||||
|
|
||||||
SwiftLint uses Azure Pipelines for most of its CI jobs, primarily because
|
|
||||||
they're the only CI provider to have a free tier with 10x concurrency on macOS.
|
|
||||||
|
|
||||||
Some CI jobs run in GitHub Actions (e.g. Docker).
|
|
||||||
|
|
||||||
Some CI jobs run on Buildkite using Mac Minis from MacStadium. These are jobs
|
|
||||||
that benefit from being run on the latest Xcode & macOS versions on bare metal.
|
|
||||||
|
|
||||||
### Buildkite Setup
|
|
||||||
|
|
||||||
To bring up a new Buildkite worker from MacStadium:
|
|
||||||
|
|
||||||
1. Change account password
|
|
||||||
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`
|
|
||||||
1. Configure and launch buildkite agent: `brew info buildkite-agent` /
|
|
||||||
https://buildkite.com/organizations/swiftlint/agents#setup-macos
|
|
||||||
|
|
11
Dangerfile
11
Dangerfile
|
@ -14,13 +14,11 @@ modified_files = git.modified_files + git.added_files
|
||||||
# including in a CHANGELOG for example
|
# including in a CHANGELOG for example
|
||||||
has_app_changes = !modified_files.grep(/Source/).empty?
|
has_app_changes = !modified_files.grep(/Source/).empty?
|
||||||
has_test_changes = !modified_files.grep(/Tests/).empty?
|
has_test_changes = !modified_files.grep(/Tests/).empty?
|
||||||
has_danger_changes = !modified_files.grep(/Dangerfile|tools\/oss-check|Gemfile/).empty?
|
has_danger_changes = !modified_files.grep(/Dangerfile|script\/oss-check|Gemfile/).empty?
|
||||||
has_package_changes = !modified_files.grep(/Package\.swift/).empty?
|
|
||||||
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD|MODULE\.bazel/).empty?
|
|
||||||
|
|
||||||
# Add a CHANGELOG entry for app changes
|
# Add a CHANGELOG entry for app changes
|
||||||
if !modified_files.include?('CHANGELOG.md') && has_app_changes
|
if !modified_files.include?('CHANGELOG.md') && has_app_changes
|
||||||
warn("If this is a user-facing change, please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/realm/SwiftLint/blob/main/CHANGELOG.md).")
|
warn("Please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/realm/SwiftLint/blob/master/CHANGELOG.md).")
|
||||||
markdown <<-MARKDOWN
|
markdown <<-MARKDOWN
|
||||||
Here's an example of your CHANGELOG entry:
|
Here's an example of your CHANGELOG entry:
|
||||||
```markdown
|
```markdown
|
||||||
|
@ -32,7 +30,7 @@ Here's an example of your CHANGELOG entry:
|
||||||
MARKDOWN
|
MARKDOWN
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless has_app_changes || has_danger_changes || has_package_changes || has_bazel_changes
|
return unless has_app_changes || has_danger_changes
|
||||||
|
|
||||||
# Non-trivial amounts of app changes without tests
|
# Non-trivial amounts of app changes without tests
|
||||||
if git.lines_of_code > 50 && has_app_changes && !has_test_changes
|
if git.lines_of_code > 50 && has_app_changes && !has_test_changes
|
||||||
|
@ -51,8 +49,7 @@ end
|
||||||
|
|
||||||
file = Tempfile.new('violations')
|
file = Tempfile.new('violations')
|
||||||
|
|
||||||
force_flag = has_danger_changes ? "--force" : ""
|
Open3.popen3("script/oss-check -v 2> #{file.path}") do |_, stdout, _, _|
|
||||||
Open3.popen3("tools/oss-check -v #{force_flag} 2> #{file.path}") do |_, stdout, _, _|
|
|
||||||
while char = stdout.getc
|
while char = stdout.getc
|
||||||
print char
|
print char
|
||||||
end
|
end
|
||||||
|
|
18
Dockerfile
18
Dockerfile
|
@ -1,6 +1,6 @@
|
||||||
# Explicitly specify `jammy` to keep the Swift & Ubuntu images in sync.
|
# Explicitly specify `focal` because `swift:latest` does not use `ubuntu:latest`.
|
||||||
ARG BUILDER_IMAGE=swift:jammy
|
ARG BUILDER_IMAGE=swift:focal
|
||||||
ARG RUNTIME_IMAGE=ubuntu:jammy
|
ARG RUNTIME_IMAGE=ubuntu:focal
|
||||||
|
|
||||||
# builder image
|
# builder image
|
||||||
FROM ${BUILDER_IMAGE} AS builder
|
FROM ${BUILDER_IMAGE} AS builder
|
||||||
|
@ -9,16 +9,16 @@ RUN apt-get update && apt-get install -y \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
&& rm -r /var/lib/apt/lists/*
|
&& rm -r /var/lib/apt/lists/*
|
||||||
WORKDIR /workdir/
|
WORKDIR /workdir/
|
||||||
COPY Plugins Plugins/
|
|
||||||
COPY Source Source/
|
COPY Source Source/
|
||||||
COPY Tests Tests/
|
COPY Tests Tests/
|
||||||
COPY Package.* ./
|
COPY Package.* ./
|
||||||
|
|
||||||
RUN swift package update
|
ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -lCFURLSessionInterface -Xlinker -lCFXMLInterface -Xlinker -lcurl -Xlinker -lxml2"
|
||||||
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
|
||||||
RUN swift build $SWIFT_FLAGS --product swiftlint
|
|
||||||
RUN mkdir -p /executables
|
RUN mkdir -p /executables
|
||||||
RUN install -v `swift build $SWIFT_FLAGS --show-bin-path`/swiftlint /executables
|
RUN for executable in $(swift package completion-tool list-executables); do \
|
||||||
|
install -v `swift build $SWIFT_FLAGS --show-bin-path`/$executable /executables; \
|
||||||
|
done
|
||||||
|
|
||||||
# runtime image
|
# runtime image
|
||||||
FROM ${RUNTIME_IMAGE}
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
@ -30,10 +30,8 @@ RUN apt-get update && apt-get install -y \
|
||||||
COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib
|
COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib
|
||||||
COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib
|
COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib
|
||||||
COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib
|
COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib
|
||||||
COPY --from=builder /usr/lib/swift/linux/libswiftCore.so /usr/lib
|
|
||||||
COPY --from=builder /executables/* /usr/bin
|
COPY --from=builder /executables/* /usr/bin
|
||||||
|
|
||||||
RUN swiftlint version
|
RUN swiftlint version
|
||||||
RUN echo "_ = 0" | swiftlint --use-stdin
|
|
||||||
|
|
||||||
CMD ["swiftlint"]
|
CMD ["swiftlint"]
|
||||||
|
|
134
Gemfile.lock
134
Gemfile.lock
|
@ -1,157 +1,159 @@
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.6)
|
CFPropertyList (3.0.3)
|
||||||
rexml
|
activesupport (5.2.5)
|
||||||
activesupport (7.0.4.3)
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.8.4)
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
algoliasearch (1.27.5)
|
algoliasearch (1.27.5)
|
||||||
httpclient (~> 2.8, >= 2.8.3)
|
httpclient (~> 2.8, >= 2.8.3)
|
||||||
json (>= 1.5.1)
|
json (>= 1.5.1)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
claide (1.1.0)
|
claide (1.0.3)
|
||||||
claide-plugins (0.9.2)
|
claide-plugins (0.9.2)
|
||||||
cork
|
cork
|
||||||
nap
|
nap
|
||||||
open4 (~> 1.3)
|
open4 (~> 1.3)
|
||||||
cocoapods (1.12.1)
|
cocoapods (1.10.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.6)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
cocoapods-core (= 1.12.1)
|
cocoapods-core (= 1.10.1)
|
||||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||||
cocoapods-downloader (>= 1.6.0, < 2.0)
|
cocoapods-downloader (>= 1.4.0, < 2.0)
|
||||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||||
cocoapods-search (>= 1.0.0, < 2.0)
|
cocoapods-search (>= 1.0.0, < 2.0)
|
||||||
cocoapods-trunk (>= 1.6.0, < 2.0)
|
cocoapods-trunk (>= 1.4.0, < 2.0)
|
||||||
cocoapods-try (>= 1.1.0, < 2.0)
|
cocoapods-try (>= 1.1.0, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
escape (~> 0.0.4)
|
escape (~> 0.0.4)
|
||||||
fourflusher (>= 2.3.0, < 3.0)
|
fourflusher (>= 2.3.0, < 3.0)
|
||||||
gh_inspector (~> 1.0)
|
gh_inspector (~> 1.0)
|
||||||
molinillo (~> 0.8.0)
|
molinillo (~> 0.6.6)
|
||||||
nap (~> 1.0)
|
nap (~> 1.0)
|
||||||
ruby-macho (>= 2.3.0, < 3.0)
|
ruby-macho (~> 1.4)
|
||||||
xcodeproj (>= 1.21.0, < 2.0)
|
xcodeproj (>= 1.19.0, < 2.0)
|
||||||
cocoapods-core (1.12.1)
|
cocoapods-core (1.10.1)
|
||||||
activesupport (>= 5.0, < 8)
|
activesupport (> 5.0, < 6)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.6)
|
||||||
algoliasearch (~> 1.0)
|
algoliasearch (~> 1.0)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
fuzzy_match (~> 2.0.4)
|
fuzzy_match (~> 2.0.4)
|
||||||
nap (~> 1.0)
|
nap (~> 1.0)
|
||||||
netrc (~> 0.11)
|
netrc (~> 0.11)
|
||||||
public_suffix (~> 4.0)
|
public_suffix
|
||||||
typhoeus (~> 1.0)
|
typhoeus (~> 1.0)
|
||||||
cocoapods-deintegrate (1.0.5)
|
cocoapods-deintegrate (1.0.4)
|
||||||
cocoapods-downloader (1.6.3)
|
cocoapods-downloader (1.4.0)
|
||||||
cocoapods-plugins (1.0.0)
|
cocoapods-plugins (1.0.0)
|
||||||
nap
|
nap
|
||||||
cocoapods-search (1.0.1)
|
cocoapods-search (1.0.0)
|
||||||
cocoapods-trunk (1.6.0)
|
cocoapods-trunk (1.5.0)
|
||||||
nap (>= 0.8, < 2.0)
|
nap (>= 0.8, < 2.0)
|
||||||
netrc (~> 0.11)
|
netrc (~> 0.11)
|
||||||
cocoapods-try (1.2.0)
|
cocoapods-try (1.2.0)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.1.8)
|
||||||
cork (0.3.0)
|
cork (0.3.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
danger (9.2.0)
|
danger (8.2.3)
|
||||||
claide (~> 1.0)
|
claide (~> 1.0)
|
||||||
claide-plugins (>= 0.9.2)
|
claide-plugins (>= 0.9.2)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
cork (~> 0.1)
|
cork (~> 0.1)
|
||||||
faraday (>= 0.9.0, < 3.0)
|
faraday (>= 0.9.0, < 2.0)
|
||||||
faraday-http-cache (~> 2.0)
|
faraday-http-cache (~> 2.0)
|
||||||
git (~> 1.7)
|
git (~> 1.7)
|
||||||
kramdown (~> 2.3)
|
kramdown (~> 2.3)
|
||||||
kramdown-parser-gfm (~> 1.0)
|
kramdown-parser-gfm (~> 1.0)
|
||||||
no_proxy_fix
|
no_proxy_fix
|
||||||
octokit (~> 5.0)
|
octokit (~> 4.7)
|
||||||
terminal-table (>= 1, < 4)
|
terminal-table (>= 1, < 4)
|
||||||
escape (0.0.4)
|
escape (0.0.4)
|
||||||
ethon (0.16.0)
|
ethon (0.13.0)
|
||||||
ffi (>= 1.15.0)
|
ffi (>= 1.15.0)
|
||||||
faraday (2.7.4)
|
faraday (1.4.1)
|
||||||
faraday-net_http (>= 2.0, < 3.1)
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.1)
|
||||||
|
multipart-post (>= 1.2, < 3)
|
||||||
ruby2_keywords (>= 0.0.4)
|
ruby2_keywords (>= 0.0.4)
|
||||||
faraday-http-cache (2.4.1)
|
faraday-excon (1.1.0)
|
||||||
|
faraday-http-cache (2.2.0)
|
||||||
faraday (>= 0.8)
|
faraday (>= 0.8)
|
||||||
faraday-net_http (3.0.2)
|
faraday-net_http (1.0.1)
|
||||||
ffi (1.15.5)
|
faraday-net_http_persistent (1.1.0)
|
||||||
|
ffi (1.15.0)
|
||||||
fourflusher (2.3.1)
|
fourflusher (2.3.1)
|
||||||
fuzzy_match (2.0.4)
|
fuzzy_match (2.0.4)
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
git (1.18.0)
|
git (1.8.1)
|
||||||
addressable (~> 2.8)
|
|
||||||
rchardet (~> 1.8)
|
rchardet (~> 1.8)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.12.0)
|
i18n (1.8.10)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
jazzy (0.14.3)
|
jazzy (0.13.6)
|
||||||
cocoapods (~> 1.5)
|
cocoapods (~> 1.5)
|
||||||
mustache (~> 1.1)
|
mustache (~> 1.1)
|
||||||
open4 (~> 1.3)
|
open4
|
||||||
redcarpet (~> 3.4)
|
redcarpet (~> 3.4)
|
||||||
rexml (~> 3.2)
|
|
||||||
rouge (>= 2.0.6, < 4.0)
|
rouge (>= 2.0.6, < 4.0)
|
||||||
sassc (~> 2.1)
|
sassc (~> 2.1)
|
||||||
sqlite3 (~> 1.3)
|
sqlite3 (~> 1.3)
|
||||||
xcinvoke (~> 0.3.0)
|
xcinvoke (~> 0.3.0)
|
||||||
json (2.6.3)
|
json (2.5.1)
|
||||||
kramdown (2.4.0)
|
kramdown (2.3.1)
|
||||||
rexml
|
rexml
|
||||||
kramdown-parser-gfm (1.1.0)
|
kramdown-parser-gfm (1.1.0)
|
||||||
kramdown (~> 2.0)
|
kramdown (~> 2.0)
|
||||||
liferaft (0.0.6)
|
liferaft (0.0.6)
|
||||||
minitest (5.18.0)
|
minitest (5.14.4)
|
||||||
molinillo (0.8.0)
|
molinillo (0.6.6)
|
||||||
|
multipart-post (2.1.1)
|
||||||
mustache (1.1.1)
|
mustache (1.1.1)
|
||||||
nanaimo (0.3.0)
|
nanaimo (0.3.0)
|
||||||
nap (1.1.0)
|
nap (1.1.0)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
no_proxy_fix (0.1.2)
|
no_proxy_fix (0.1.2)
|
||||||
octokit (5.6.1)
|
octokit (4.20.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 0.9)
|
||||||
sawyer (~> 0.9)
|
sawyer (~> 0.8.0, >= 0.5.3)
|
||||||
open4 (1.3.4)
|
open4 (1.3.4)
|
||||||
public_suffix (4.0.7)
|
public_suffix (4.0.6)
|
||||||
rchardet (1.8.0)
|
rchardet (1.8.0)
|
||||||
redcarpet (3.6.0)
|
redcarpet (3.5.1)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rouge (3.30.0)
|
rouge (3.26.0)
|
||||||
ruby-macho (2.5.1)
|
ruby-macho (1.4.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.4)
|
||||||
sassc (2.4.0)
|
sassc (2.4.0)
|
||||||
ffi (~> 1.9)
|
ffi (~> 1.9)
|
||||||
sawyer (0.9.2)
|
sawyer (0.8.2)
|
||||||
addressable (>= 2.3.5)
|
addressable (>= 2.3.5)
|
||||||
faraday (>= 0.17.3, < 3)
|
faraday (> 0.8, < 2.0)
|
||||||
sqlite3 (1.6.2-arm64-darwin)
|
sqlite3 (1.4.2)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.0)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
|
thread_safe (0.3.6)
|
||||||
typhoeus (1.4.0)
|
typhoeus (1.4.0)
|
||||||
ethon (>= 0.9.0)
|
ethon (>= 0.9.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (1.2.9)
|
||||||
concurrent-ruby (~> 1.0)
|
thread_safe (~> 0.1)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (1.7.0)
|
||||||
xcinvoke (0.3.0)
|
xcinvoke (0.3.0)
|
||||||
liferaft (~> 0.0.6)
|
liferaft (~> 0.0.6)
|
||||||
xcodeproj (1.22.0)
|
xcodeproj (1.19.0)
|
||||||
CFPropertyList (>= 2.3.3, < 4.0)
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
atomos (~> 0.1.3)
|
atomos (~> 0.1.3)
|
||||||
claide (>= 1.0.2, < 2.0)
|
claide (>= 1.0.2, < 2.0)
|
||||||
colored2 (~> 3.1)
|
colored2 (~> 3.1)
|
||||||
nanaimo (~> 0.3.0)
|
nanaimo (~> 0.3.0)
|
||||||
rexml (~> 3.2.4)
|
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-21
|
ruby
|
||||||
arm64-darwin-22
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
cocoapods
|
cocoapods
|
||||||
|
@ -159,4 +161,4 @@ DEPENDENCIES
|
||||||
jazzy
|
jazzy
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.12
|
2.1.4
|
||||||
|
|
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")
|
|
107
Makefile
107
Makefile
|
@ -6,12 +6,21 @@ XCODEFLAGS=-scheme 'swiftlint' \
|
||||||
DSTROOT=$(TEMPORARY_FOLDER) \
|
DSTROOT=$(TEMPORARY_FOLDER) \
|
||||||
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
|
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
|
||||||
|
|
||||||
SWIFT_BUILD_FLAGS=--configuration release -Xlinker -dead_strip
|
SWIFT_BUILD_FLAGS=--configuration release
|
||||||
|
UNAME=$(shell uname)
|
||||||
|
ifeq ($(UNAME), Darwin)
|
||||||
|
USE_SWIFT_STATIC_STDLIB:=$(shell test -d $$(dirname $$(xcrun --find swift))/../lib/swift_static/macosx && echo yes)
|
||||||
|
ifeq ($(USE_SWIFT_STATIC_STDLIB), yes)
|
||||||
|
SWIFT_BUILD_FLAGS+= -Xswiftc -static-stdlib
|
||||||
|
endif
|
||||||
|
# SwiftPM 5.3 uses the `XCBuild.framework` to generate a universal binary when it receives multiple `--arch` options.
|
||||||
|
SWIFT_BUILD_ARCHS:= arm64 x86_64
|
||||||
|
# If SwiftPM supports `--arch $(1)` and `swiftc` succeeds in building with `-target $(1)-apple-macos10.9`, then produce `--arch $(1)`.
|
||||||
|
ARCH_OPTION=$(shell swift build --show-bin-path --arch $(1) &>/dev/null && echo ''|swiftc -target $(1)-apple-macos10.9 - -o /dev/null &>/dev/null && echo "--arch" $(1))
|
||||||
|
SWIFT_BUILD_FLAGS+=$(foreach arch,$(SWIFT_BUILD_ARCHS),$(call ARCH_OPTION,$(arch)))
|
||||||
|
endif # Darwin
|
||||||
|
|
||||||
SWIFTLINT_EXECUTABLE_PARENT=.build/universal
|
SWIFTLINT_EXECUTABLE=$(shell swift build $(SWIFT_BUILD_FLAGS) --show-bin-path)/swiftlint
|
||||||
SWIFTLINT_EXECUTABLE=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint
|
|
||||||
|
|
||||||
ARTIFACT_BUNDLE_PATH=$(TEMPORARY_FOLDER)/SwiftLintBinary.artifactbundle
|
|
||||||
|
|
||||||
TSAN_LIB=$(subst bin/swift,lib/swift/clang/lib/darwin/libclang_rt.tsan_osx_dynamic.dylib,$(shell xcrun --find swift))
|
TSAN_LIB=$(subst bin/swift,lib/swift/clang/lib/darwin/libclang_rt.tsan_osx_dynamic.dylib,$(shell xcrun --find swift))
|
||||||
TSAN_SWIFT_BUILD_FLAGS=-Xswiftc -sanitize=thread
|
TSAN_SWIFT_BUILD_FLAGS=-Xswiftc -sanitize=thread
|
||||||
|
@ -24,25 +33,25 @@ LICENSE_PATH="$(shell pwd)/LICENSE"
|
||||||
|
|
||||||
OUTPUT_PACKAGE=SwiftLint.pkg
|
OUTPUT_PACKAGE=SwiftLint.pkg
|
||||||
|
|
||||||
VERSION_STRING=$(shell ./tools/get-version)
|
VERSION_STRING="$(shell ./script/get-version)"
|
||||||
|
|
||||||
.PHONY: all clean build install package test uninstall docs
|
.PHONY: all clean build install package test uninstall docs
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
sourcery: Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift Source/SwiftLintCore/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift
|
sourcery: Source/SwiftLintFramework/Models/PrimaryRuleList.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift Tests/LinuxMain.swift
|
||||||
|
|
||||||
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil
|
Tests/LinuxMain.swift: Tests/*/*.swift .sourcery/LinuxMain.stencil
|
||||||
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
|
sourcery --sources Tests --exclude-sources Tests/SwiftLintFrameworkTests/Resources --templates .sourcery/LinuxMain.stencil --output .sourcery --force-parse generated
|
||||||
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
|
mv .sourcery/LinuxMain.generated.swift Tests/LinuxMain.swift
|
||||||
|
|
||||||
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil
|
Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
|
||||||
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery
|
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
|
||||||
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.swift
|
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
|
||||||
|
|
||||||
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLint*/Rules/**/*.swift .sourcery/GeneratedTests.stencil
|
Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/AutomaticRuleTests.stencil
|
||||||
./tools/sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
|
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/AutomaticRuleTests.stencil --output .sourcery
|
||||||
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
|
mv .sourcery/AutomaticRuleTests.generated.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
|
||||||
|
|
||||||
test: clean_xcode
|
test: clean_xcode
|
||||||
$(BUILD_TOOL) $(XCODEFLAGS) test
|
$(BUILD_TOOL) $(XCODEFLAGS) test
|
||||||
|
@ -63,19 +72,14 @@ analyze_autocorrect: write_xcodebuild_log
|
||||||
clean:
|
clean:
|
||||||
rm -f "$(OUTPUT_PACKAGE)"
|
rm -f "$(OUTPUT_PACKAGE)"
|
||||||
rm -rf "$(TEMPORARY_FOLDER)"
|
rm -rf "$(TEMPORARY_FOLDER)"
|
||||||
rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256"
|
rm -f "./portable_swiftlint.zip"
|
||||||
swift package clean
|
swift package clean
|
||||||
|
|
||||||
clean_xcode:
|
clean_xcode:
|
||||||
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
|
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
|
||||||
|
|
||||||
build:
|
build:
|
||||||
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)"
|
swift build $(SWIFT_BUILD_FLAGS)
|
||||||
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)"
|
|
||||||
strip -rSTX "$(SWIFTLINT_EXECUTABLE)"
|
|
||||||
|
|
||||||
build_with_disable_sandbox:
|
build_with_disable_sandbox:
|
||||||
swift build --disable-sandbox $(SWIFT_BUILD_FLAGS)
|
swift build --disable-sandbox $(SWIFT_BUILD_FLAGS)
|
||||||
|
@ -101,13 +105,6 @@ portable_zip: installables
|
||||||
cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)"
|
cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)"
|
||||||
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip"
|
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip"
|
||||||
|
|
||||||
spm_artifactbundle_macos: installables
|
|
||||||
mkdir -p "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin"
|
|
||||||
sed 's/__VERSION__/$(VERSION_STRING)/g' tools/info-macos.json.template > "$(ARTIFACT_BUNDLE_PATH)/info.json"
|
|
||||||
cp -f "$(SWIFTLINT_EXECUTABLE)" "$(ARTIFACT_BUNDLE_PATH)/swiftlint-$(VERSION_STRING)-macos/bin"
|
|
||||||
cp -f "$(LICENSE_PATH)" "$(ARTIFACT_BUNDLE_PATH)"
|
|
||||||
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "SwiftLintBinary.artifactbundle") > "./SwiftLintBinary-macos.artifactbundle.zip"
|
|
||||||
|
|
||||||
zip_linux: docker_image
|
zip_linux: docker_image
|
||||||
$(eval TMP_FOLDER := $(shell mktemp -d))
|
$(eval TMP_FOLDER := $(shell mktemp -d))
|
||||||
docker run swiftlint cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
|
docker run swiftlint cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
|
||||||
|
@ -115,36 +112,24 @@ zip_linux: docker_image
|
||||||
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
|
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
|
||||||
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
|
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
|
||||||
|
|
||||||
zip_linux_release:
|
package: installables
|
||||||
$(eval TMP_FOLDER := $(shell mktemp -d))
|
|
||||||
docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
|
|
||||||
chmod +x "$(TMP_FOLDER)/swiftlint"
|
|
||||||
cp -f "$(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))
|
|
||||||
cp "$(SWIFTLINT_EXECUTABLE)" "$(PACKAGE_ROOT)"
|
|
||||||
pkgbuild \
|
pkgbuild \
|
||||||
--identifier "io.realm.swiftlint" \
|
--identifier "io.realm.swiftlint" \
|
||||||
--install-location "/usr/local/bin" \
|
--install-location "/usr/local/bin" \
|
||||||
--root "$(PACKAGE_ROOT)" \
|
--root "$(TEMPORARY_FOLDER)" \
|
||||||
--version "$(VERSION_STRING)" \
|
--version "$(VERSION_STRING)" \
|
||||||
"$(OUTPUT_PACKAGE)"
|
"$(OUTPUT_PACKAGE)"
|
||||||
|
|
||||||
bazel_release:
|
release: package portable_zip zip_linux
|
||||||
bazel build :release
|
|
||||||
mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 .
|
|
||||||
|
|
||||||
docker_image:
|
docker_image:
|
||||||
docker build --platform linux/amd64 --force-rm --tag swiftlint .
|
docker build --force-rm --tag swiftlint .
|
||||||
|
|
||||||
docker_test:
|
docker_test:
|
||||||
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.7-focal swift test --parallel
|
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.4 swift test --parallel
|
||||||
|
|
||||||
docker_htop:
|
docker_htop:
|
||||||
docker run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset
|
docker run -it --rm --pid=container:swiftlint terencewestphal/htop || reset
|
||||||
|
|
||||||
# https://irace.me/swift-profiling
|
# https://irace.me/swift-profiling
|
||||||
display_compilation_time:
|
display_compilation_time:
|
||||||
|
@ -152,8 +137,8 @@ display_compilation_time:
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
|
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
|
||||||
bundle install
|
pod trunk push SwiftLintFramework.podspec
|
||||||
bundle exec pod trunk push SwiftLint.podspec
|
pod trunk push SwiftLint.podspec
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
swift run swiftlint generate-docs
|
swift run swiftlint generate-docs
|
||||||
|
@ -161,32 +146,20 @@ docs:
|
||||||
bundle exec jazzy
|
bundle exec jazzy
|
||||||
|
|
||||||
get_version:
|
get_version:
|
||||||
@echo "$(VERSION_STRING)"
|
@echo $(VERSION_STRING)
|
||||||
|
|
||||||
release:
|
push_version:
|
||||||
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
|
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
|
||||||
$(error git state is not clean)
|
$(error git state is not clean)
|
||||||
endif
|
endif
|
||||||
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
|
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
|
||||||
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
|
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
|
||||||
@sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
|
@sed -i '' 's/## Master/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
|
||||||
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintCore/Models/Version.swift
|
@sed 's/__VERSION__/$(NEW_VERSION)/g' script/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
|
||||||
@sed -e '3s/.*/ version = "$(NEW_VERSION)",/' -i '' MODULE.bazel
|
|
||||||
make clean
|
|
||||||
make package
|
|
||||||
make bazel_release
|
|
||||||
make portable_zip
|
|
||||||
make spm_artifactbundle_macos
|
|
||||||
./tools/update-artifact-bundle.sh "$(NEW_VERSION)"
|
|
||||||
git commit -a -m "release $(NEW_VERSION)"
|
git commit -a -m "release $(NEW_VERSION)"
|
||||||
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
|
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
|
||||||
git push origin HEAD
|
git push origin master
|
||||||
git push origin $(NEW_VERSION)
|
git push origin $(NEW_VERSION)
|
||||||
./tools/create-github-release.sh "$(NEW_VERSION)"
|
|
||||||
make publish
|
|
||||||
./tools/add-new-changelog-section.sh
|
|
||||||
git commit -a -m "Add new changelog section"
|
|
||||||
git push origin HEAD
|
|
||||||
|
|
||||||
%:
|
%:
|
||||||
@:
|
@:
|
||||||
|
|
132
Package.resolved
132
Package.resolved
|
@ -1,77 +1,61 @@
|
||||||
{
|
{
|
||||||
"pins" : [
|
"object": {
|
||||||
{
|
"pins": [
|
||||||
"identity" : "collectionconcurrencykit",
|
{
|
||||||
"kind" : "remoteSourceControl",
|
"package": "SourceKitten",
|
||||||
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
|
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
|
||||||
"state" : {
|
"state": {
|
||||||
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
|
"branch": null,
|
||||||
"version" : "0.2.0"
|
"revision": "558628392eb31d37cb251cfe626c53eafd330df6",
|
||||||
|
"version": "0.31.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "swift-argument-parser",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "d2930e8fcf9c33162b9fcc1d522bc975e2d4179b",
|
||||||
|
"version": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SwiftSyntax",
|
||||||
|
"repositoryURL": "https://github.com/apple/swift-syntax.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "cf40be70deaf4ce7d44eb1a7e14299c391e2363f",
|
||||||
|
"version": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SwiftyTextTable",
|
||||||
|
"repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
|
||||||
|
"version": "0.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SWXMLHash",
|
||||||
|
"repositoryURL": "https://github.com/drmohundro/SWXMLHash.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "a4931e5c3bafbedeb1601d3bb76bbe835c6d475a",
|
||||||
|
"version": "5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "Yams",
|
||||||
|
"repositoryURL": "https://github.com/jpsim/Yams.git",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "51ef46468fda5a0fa1a201b8843791d0149d3c01",
|
||||||
|
"version": "4.0.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
{
|
},
|
||||||
"identity" : "cryptoswift",
|
"version": 1
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f",
|
|
||||||
"version" : "1.7.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sourcekitten",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/jpsim/SourceKitten.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
|
|
||||||
"version" : "0.34.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-argument-parser",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
|
|
||||||
"version" : "1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-syntax",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-syntax.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5",
|
|
||||||
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swiftytexttable",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/scottrhoyt/SwiftyTextTable.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
|
|
||||||
"version" : "0.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swxmlhash",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/drmohundro/SWXMLHash.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4d0f62f561458cbe1f732171e625f03195151b60",
|
|
||||||
"version" : "7.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "yams",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/jpsim/Yams.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a",
|
|
||||||
"version" : "5.0.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 2
|
|
||||||
}
|
}
|
||||||
|
|
113
Package.swift
113
Package.swift
|
@ -1,121 +1,58 @@
|
||||||
// swift-tools-version:5.7
|
// swift-tools-version:5.4
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
|
#if canImport(CommonCrypto)
|
||||||
|
private let addCryptoSwift = false
|
||||||
|
#else
|
||||||
|
private let addCryptoSwift = true
|
||||||
|
#endif
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "SwiftLint",
|
name: "SwiftLint",
|
||||||
platforms: [.macOS(.v12)],
|
platforms: [.macOS(.v10_12)],
|
||||||
products: [
|
products: [
|
||||||
.executable(name: "swiftlint", targets: ["swiftlint"]),
|
.executable(name: "swiftlint", targets: ["swiftlint"]),
|
||||||
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]),
|
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"])
|
||||||
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")),
|
.package(name: "swift-argument-parser", url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.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", from: "0.31.1"),
|
||||||
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")),
|
.package(url: "https://github.com/jpsim/Yams.git", from: "4.0.2"),
|
||||||
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"),
|
|
||||||
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
|
||||||
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"),
|
.package(name: "SwiftSyntax", url: "https://github.com/apple/swift-syntax.git",
|
||||||
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.7.2"))
|
.revision("cf40be70deaf4ce7d44eb1a7e14299c391e2363f")),
|
||||||
],
|
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.3.2"))] : []),
|
||||||
targets: [
|
targets: [
|
||||||
.plugin(
|
|
||||||
name: "SwiftLintPlugin",
|
|
||||||
capability: .buildTool(),
|
|
||||||
dependencies: [
|
|
||||||
.target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),
|
|
||||||
.target(name: "swiftlint", condition: .when(platforms: [.linux]))
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "swiftlint",
|
name: "swiftlint",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
"CollectionConcurrencyKit",
|
|
||||||
"SwiftLintFramework",
|
"SwiftLintFramework",
|
||||||
"SwiftyTextTable",
|
"SwiftyTextTable",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "CLITests",
|
|
||||||
dependencies: [
|
|
||||||
"swiftlint"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SwiftLintCore",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux])),
|
|
||||||
.target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])),
|
|
||||||
.product(name: "SourceKittenFramework", package: "SourceKitten"),
|
|
||||||
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
|
|
||||||
.product(name: "SwiftOperators", package: "swift-syntax"),
|
|
||||||
.product(name: "SwiftParser", package: "swift-syntax"),
|
|
||||||
.product(name: "SwiftSyntax", package: "swift-syntax"),
|
|
||||||
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
|
|
||||||
.product(name: "SwiftyTextTable", package: "SwiftyTextTable"),
|
|
||||||
.product(name: "Yams", package: "Yams"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SwiftLintBuiltInRules",
|
|
||||||
dependencies: ["SwiftLintCore"]
|
|
||||||
),
|
|
||||||
.target(
|
|
||||||
name: "SwiftLintExtraRules",
|
|
||||||
dependencies: ["SwiftLintCore"]
|
|
||||||
),
|
|
||||||
.target(
|
.target(
|
||||||
name: "SwiftLintFramework",
|
name: "SwiftLintFramework",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"SwiftLintBuiltInRules",
|
.product(name: "SourceKittenFramework", package: "SourceKitten"),
|
||||||
"SwiftLintCore",
|
"lib_InternalSwiftSyntaxParser",
|
||||||
"SwiftLintExtraRules"
|
"SwiftSyntax",
|
||||||
]
|
"Yams",
|
||||||
),
|
] + (addCryptoSwift ? ["CryptoSwift"] : [])
|
||||||
.target(name: "DyldWarningWorkaround"),
|
|
||||||
.target(
|
|
||||||
name: "SwiftLintTestHelpers",
|
|
||||||
dependencies: [
|
|
||||||
"SwiftLintFramework"
|
|
||||||
],
|
|
||||||
path: "Tests/SwiftLintTestHelpers"
|
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "SwiftLintFrameworkTests",
|
name: "SwiftLintFrameworkTests",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"SwiftLintFramework",
|
"SwiftLintFramework"
|
||||||
"SwiftLintTestHelpers"
|
|
||||||
],
|
],
|
||||||
exclude: [
|
exclude: [
|
||||||
"Resources",
|
"Resources",
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(
|
|
||||||
name: "GeneratedTests",
|
|
||||||
dependencies: [
|
|
||||||
"SwiftLintFramework",
|
|
||||||
"SwiftLintTestHelpers"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "IntegrationTests",
|
|
||||||
dependencies: [
|
|
||||||
"SwiftLintFramework",
|
|
||||||
"SwiftLintTestHelpers"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(
|
|
||||||
name: "ExtraRulesTests",
|
|
||||||
dependencies: [
|
|
||||||
"SwiftLintFramework",
|
|
||||||
"SwiftLintTestHelpers"
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.binaryTarget(
|
.binaryTarget(
|
||||||
name: "SwiftLintBinary",
|
name: "lib_InternalSwiftSyntaxParser",
|
||||||
url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
|
url: "https://github.com/keith/StaticInternalSwiftSyntaxParser/releases/download/5.5.2/lib_InternalSwiftSyntaxParser.xcframework.zip",
|
||||||
checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"
|
checksum: "96bbc9ab4679953eac9ee46778b498cb559b8a7d9ecc658e54d6679acfbb34b8"
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
|
280
README.md
280
README.md
|
@ -1,14 +1,14 @@
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide).
|
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Ray Wenderlich's Swift Style Guide](https://github.com/raywenderlich/swift-style-guide).
|
||||||
|
|
||||||
SwiftLint hooks into [Clang](http://clang.llvm.org) and
|
SwiftLint hooks into [Clang](http://clang.llvm.org) and
|
||||||
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
|
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
|
||||||
[AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation
|
[AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation
|
||||||
of your source files for more accurate results.
|
of your source files for more accurate results.
|
||||||
|
|
||||||
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
|
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
|
||||||
[](https://codecov.io/github/realm/SwiftLint?branch=main)
|
[](https://codecov.io/github/realm/SwiftLint?branch=master)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ This project adheres to the [Contributor Covenant Code of Conduct](https://realm
|
||||||
By participating, you are expected to uphold this code. Please report
|
By participating, you are expected to uphold this code. Please report
|
||||||
unacceptable behavior to [info@realm.io](mailto:info@realm.io).
|
unacceptable behavior to [info@realm.io](mailto:info@realm.io).
|
||||||
|
|
||||||
> Language Switch: [中文](https://github.com/realm/SwiftLint/blob/main/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/main/README_KR.md).
|
> Language Switch: [中文](https://github.com/realm/SwiftLint/blob/master/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/master/README_KR.md).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ in your Script Build Phases.
|
||||||
This is the recommended way to install a specific version of SwiftLint since it supports
|
This is the recommended way to install a specific version of SwiftLint since it supports
|
||||||
installing a pinned version rather than simply the latest (which is the case with Homebrew).
|
installing a pinned version rather than simply the latest (which is the case with Homebrew).
|
||||||
|
|
||||||
Note that this will add the SwiftLint binaries, its dependencies' binaries, and the Swift binary
|
Note that this will add the SwiftLint binaries, its dependencies' binaries and the Swift binary
|
||||||
library distribution to the `Pods/` directory, so checking in this directory to SCM such as
|
library distribution to the `Pods/` directory, so checking in this directory to SCM such as
|
||||||
git is discouraged.
|
git is discouraged.
|
||||||
|
|
||||||
|
@ -60,74 +60,7 @@ running it.
|
||||||
### Installing from source:
|
### Installing from source:
|
||||||
|
|
||||||
You can also build and install from source by cloning this project and running
|
You can also build and install from source by cloning this project and running
|
||||||
`make install` (Xcode 13.3 or later).
|
`make install` (Xcode 12.5 or later).
|
||||||
|
|
||||||
### 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`:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
|
|
||||||
<summary>WORKSPACE</summary>
|
|
||||||
|
|
||||||
```bzl
|
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
load(
|
|
||||||
"@build_bazel_rules_apple//apple:repositories.bzl",
|
|
||||||
"apple_rules_dependencies",
|
|
||||||
)
|
|
||||||
|
|
||||||
apple_rules_dependencies()
|
|
||||||
|
|
||||||
load(
|
|
||||||
"@build_bazel_rules_swift//swift:repositories.bzl",
|
|
||||||
"swift_rules_dependencies",
|
|
||||||
)
|
|
||||||
|
|
||||||
swift_rules_dependencies()
|
|
||||||
|
|
||||||
load(
|
|
||||||
"@build_bazel_rules_swift//swift:extras.bzl",
|
|
||||||
"swift_rules_extra_dependencies",
|
|
||||||
)
|
|
||||||
|
|
||||||
swift_rules_extra_dependencies()
|
|
||||||
|
|
||||||
http_archive(
|
|
||||||
name = "SwiftLint",
|
|
||||||
sha256 = "7c454ff4abeeecdd9513f6293238a6d9f803b587eb93de147f9aa1be0d8337c4",
|
|
||||||
url = "https://github.com/realm/SwiftLint/releases/download/0.49.1/bazel.tar.gz",
|
|
||||||
)
|
|
||||||
|
|
||||||
load("@SwiftLint//bazel:repos.bzl", "swiftlint_repos")
|
|
||||||
|
|
||||||
swiftlint_repos()
|
|
||||||
|
|
||||||
load("@SwiftLint//bazel:deps.bzl", "swiftlint_deps")
|
|
||||||
|
|
||||||
swiftlint_deps()
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
Then you can run SwiftLint in the current directory with this command:
|
|
||||||
|
|
||||||
```console
|
|
||||||
bazel run -c opt @SwiftLint//:swiftlint
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -136,54 +69,38 @@ bazel run -c opt @SwiftLint//:swiftlint
|
||||||
To get a high-level overview of recommended ways to integrate SwiftLint into your project,
|
To get a high-level overview of recommended ways to integrate SwiftLint into your project,
|
||||||
we encourage you to watch this presentation or read the transcript:
|
we encourage you to watch this presentation or read the transcript:
|
||||||
|
|
||||||
[](https://youtu.be/9Z1nTMTejqU)
|
[](https://academy.realm.io/posts/slug-jp-simard-swiftlint/)
|
||||||
|
|
||||||
### Xcode
|
### Xcode
|
||||||
|
|
||||||
Integrate SwiftLint into your Xcode project to get warnings and errors displayed
|
Integrate SwiftLint into your Xcode project to get warnings and errors displayed
|
||||||
in the issue navigator.
|
in the issue navigator.
|
||||||
|
|
||||||
To do this select the project in the file navigator, then select the primary app
|
To do this click the Project in the file navigator, then click the primary app
|
||||||
target, and go to Build Phases. Click the + and select "New Run Script Phase".
|
target, and go to Build Phases. Click the + and select "New Run Script Phase".
|
||||||
Insert the following as the script:
|
Insert the following as the script:
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If you installed SwiftLint via Homebrew on Apple Silicon, you might experience this warning:
|
|
||||||
|
|
||||||
> warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint
|
|
||||||
|
|
||||||
That is because Homebrew on Apple Silicon installs the binaries into the `/opt/homebrew/bin`
|
|
||||||
folder by default. To instruct Xcode where to find SwiftLint, you can either add
|
|
||||||
`/opt/homebrew/bin` to the `PATH` environment variable in your build phase
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
if [[ "$(uname -m)" == arm64 ]]; then
|
export PATH="$PATH:/opt/homebrew/bin"
|
||||||
export PATH="/opt/homebrew/bin:$PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if which swiftlint > /dev/null; then
|
if which swiftlint >/dev/null; then
|
||||||
swiftlint
|
swiftlint
|
||||||
else
|
else
|
||||||
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
|
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
or you can create a symbolic link in `/usr/local/bin` pointing to the actual binary:
|

|
||||||
|
|
||||||
```bash
|
You might want to move your SwiftLint phase directly before 'Compile Sources'
|
||||||
ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint
|
step, to detect errors quickly before compiling. However, SwiftLint is designed
|
||||||
```
|
|
||||||
|
|
||||||
You might want to move your SwiftLint phase directly before the 'Compile Sources'
|
|
||||||
step to detect errors quickly before compiling. However, SwiftLint is designed
|
|
||||||
to run on valid Swift code that cleanly completes the compiler's parsing stage.
|
to run on valid Swift code that cleanly completes the compiler's parsing stage.
|
||||||
So running SwiftLint before 'Compile Sources' might yield some incorrect
|
So running SwiftLint before 'Compile Sources' might yield some incorrect
|
||||||
results.
|
results.
|
||||||
|
|
||||||
If you wish to fix violations as well, your script could run
|
If you wish to fix violations as well, your script could run
|
||||||
`swiftlint --fix && swiftlint` instead of just `swiftlint`. This will mean
|
`swiftlint --fix && swiftlint` instead of just `swiftlint`. This will mean
|
||||||
that all correctable violations are fixed while ensuring warnings show up in
|
that all correctable violations are fixed, while ensuring warnings show up in
|
||||||
your project for remaining violations.
|
your project for remaining violations.
|
||||||
|
|
||||||
If you've installed SwiftLint via CocoaPods the script should look like this:
|
If you've installed SwiftLint via CocoaPods the script should look like this:
|
||||||
|
@ -192,52 +109,27 @@ If you've installed SwiftLint via CocoaPods the script should look like this:
|
||||||
"${PODS_ROOT}/SwiftLint/swiftlint"
|
"${PODS_ROOT}/SwiftLint/swiftlint"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Plug-in Support
|
#### Format on Save Xcode Plugin
|
||||||
|
|
||||||
SwiftLint can be used as a build tool plug-in for both Xcode projects as well as
|
To run `swiftlint --fix` on save in Xcode, install the
|
||||||
Swift packages.
|
[SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) plugin from Alcatraz.
|
||||||
|
|
||||||
> Due to limitations with Swift Package Manager Plug-ins this is only
|
⚠️This plugin will not work with Xcode 8 or later without disabling SIP.
|
||||||
recommended for projects that have a SwiftLint configuration in their root directory as
|
This is not recommended.
|
||||||
there is currently no way to pass any additional options to the SwiftLint executable.
|
|
||||||
|
|
||||||
#### Xcode
|
### AppCode
|
||||||
|
|
||||||
You can integrate SwiftLint as an Xcode Build Tool Plug-in if you're working
|
To integrate SwiftLint with AppCode, install
|
||||||
with a project in Xcode.
|
[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 `⌥⏎`.
|
||||||
|
|
||||||
Add SwiftLint as a package dependency to your project without linking any of the
|
### Atom
|
||||||
products.
|
|
||||||
|
|
||||||
Select the target you want to add linting to and open the `Build Phases` inspector.
|
To integrate SwiftLint with [Atom](https://atom.io/), install the
|
||||||
Open `Run Build Tool Plug-ins` and select the `+` button.
|
[`linter-swiftlint`](https://atom.io/packages/linter-swiftlint) package from
|
||||||
Select `SwiftLintPlugin` from the list and add it to the project.
|
APM.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
For unattended use (e.g. on CI), you can disable the package validation dialog by
|
|
||||||
|
|
||||||
* individually passing `-skipPackagePluginValidation` to `xcodebuild` or
|
|
||||||
* globally setting `defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES`
|
|
||||||
for that user.
|
|
||||||
|
|
||||||
_Note: This implicitly trusts all Xcode package plugins and bypasses Xcode's package validation
|
|
||||||
dialogs, which has security implications._
|
|
||||||
|
|
||||||
#### Swift Package
|
|
||||||
|
|
||||||
You can integrate SwiftLint as a Swift Package Manager Plug-in if you're working with
|
|
||||||
a Swift Package with a `Package.swift` manifest.
|
|
||||||
|
|
||||||
Add SwiftLint as a package dependency to your `Package.swift` file.
|
|
||||||
Add SwiftLint to a target using the `plugins` parameter.
|
|
||||||
|
|
||||||
```swift
|
|
||||||
.target(
|
|
||||||
...
|
|
||||||
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
|
|
||||||
),
|
|
||||||
```
|
|
||||||
|
|
||||||
### Visual Studio Code
|
### Visual Studio Code
|
||||||
|
|
||||||
|
@ -308,7 +200,6 @@ SUBCOMMANDS:
|
||||||
docs Open SwiftLint documentation website in the default web browser
|
docs Open SwiftLint documentation website in the default web browser
|
||||||
generate-docs Generates markdown documentation for all rules
|
generate-docs Generates markdown documentation for all rules
|
||||||
lint (default) Print lint warnings and errors
|
lint (default) Print lint warnings and errors
|
||||||
reporters Display the list of reporters and their identifiers
|
|
||||||
rules Display the list of rules and their identifiers
|
rules Display the list of rules and their identifiers
|
||||||
version Display the current version of SwiftLint
|
version Display the current version of SwiftLint
|
||||||
|
|
||||||
|
@ -376,21 +267,12 @@ Once [installed](https://pre-commit.com/#install), add this to the
|
||||||
```yaml
|
```yaml
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/realm/SwiftLint
|
- repo: https://github.com/realm/SwiftLint
|
||||||
rev: 0.50.3
|
rev: 0.44.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: swiftlint
|
- id: swiftlint
|
||||||
```
|
```
|
||||||
|
|
||||||
Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` can be used to update to the current version.
|
Adjust `rev` to the SwiftLint version of your choice.
|
||||||
|
|
||||||
SwiftLint can be configured using `entry` to apply fixes and fail on errors:
|
|
||||||
```yaml
|
|
||||||
- repo: https://github.com/realm/SwiftLint
|
|
||||||
rev: 0.50.3
|
|
||||||
hooks:
|
|
||||||
- id: swiftlint
|
|
||||||
entry: swiftlint --fix --strict
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
|
|
||||||
|
@ -401,7 +283,7 @@ continues to contribute more over time.
|
||||||
You can find an updated list of rules and more information about them
|
You can find an updated list of rules and more information about them
|
||||||
[here](https://realm.github.io/SwiftLint/rule-directory.html).
|
[here](https://realm.github.io/SwiftLint/rule-directory.html).
|
||||||
|
|
||||||
You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules)
|
You can also check [Source/SwiftLintFramework/Rules](https://github.com/realm/SwiftLint/tree/master/Source/SwiftLintFramework/Rules)
|
||||||
directory to see their implementation.
|
directory to see their implementation.
|
||||||
|
|
||||||
### Opt-In Rules
|
### Opt-In Rules
|
||||||
|
@ -479,9 +361,7 @@ run SwiftLint from. The following parameters can be configured:
|
||||||
Rule inclusion:
|
Rule inclusion:
|
||||||
|
|
||||||
* `disabled_rules`: Disable rules from the default enabled set.
|
* `disabled_rules`: Disable rules from the default enabled set.
|
||||||
* `opt_in_rules`: Enable rules that are not part of the default set. The
|
* `opt_in_rules`: Enable rules not from the default set.
|
||||||
special `all` identifier will enable all opt in linter rules, except the ones
|
|
||||||
listed in `disabled_rules`.
|
|
||||||
* `only_rules`: Only the rules specified in this list will be enabled.
|
* `only_rules`: Only the rules specified in this list will be enabled.
|
||||||
Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
|
Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
|
||||||
* `analyzer_rules`: This is an entirely separate list of rules that are only
|
* `analyzer_rules`: This is an entirely separate list of rules that are only
|
||||||
|
@ -503,9 +383,6 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
|
||||||
# - empty_parameters
|
# - empty_parameters
|
||||||
# - vertical_whitespace
|
# - vertical_whitespace
|
||||||
|
|
||||||
analyzer_rules: # Rules run by `swiftlint analyze`
|
|
||||||
- explicit_self
|
|
||||||
|
|
||||||
included: # paths to include during linting. `--path` is ignored if present.
|
included: # paths to include during linting. `--path` is ignored if present.
|
||||||
- Source
|
- Source
|
||||||
excluded: # paths to ignore during linting. Takes precedence over `included`.
|
excluded: # paths to ignore during linting. Takes precedence over `included`.
|
||||||
|
@ -514,9 +391,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
|
||||||
- Source/ExcludedFolder
|
- Source/ExcludedFolder
|
||||||
- Source/ExcludedFile.swift
|
- Source/ExcludedFile.swift
|
||||||
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
|
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
|
||||||
|
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
|
||||||
# If true, SwiftLint will not fail if no lintable files are found.
|
- explicit_self
|
||||||
allow_zero_lintable_files: false
|
|
||||||
|
|
||||||
# configurable rules can be customized from this configuration file
|
# configurable rules can be customized from this configuration file
|
||||||
# binary rules can set their severity level
|
# binary rules can set their severity level
|
||||||
|
@ -550,29 +426,13 @@ identifier_name:
|
||||||
- id
|
- id
|
||||||
- URL
|
- URL
|
||||||
- GlobalAPIKey
|
- GlobalAPIKey
|
||||||
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
|
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use environment variables in your configuration file,
|
You can also use environment variables in your configuration file,
|
||||||
by using `${SOME_VARIABLE}` in a string.
|
by using `${SOME_VARIABLE}` in a string.
|
||||||
|
|
||||||
### Defining Custom Rules
|
#### Defining Custom Rules
|
||||||
|
|
||||||
In addition to the rules that the main SwiftLint project ships with, SwiftLint
|
|
||||||
can also run two types of custom rules that you can define yourself in your own
|
|
||||||
projects:
|
|
||||||
|
|
||||||
#### 1. Swift Custom Rules
|
|
||||||
|
|
||||||
These rules are written the same way as the Swift-based rules that ship with
|
|
||||||
SwiftLint so they're fast, accurate, can leverage SwiftSyntax, can be unit
|
|
||||||
tested, and more.
|
|
||||||
|
|
||||||
Using these requires building SwiftLint with Bazel as described in
|
|
||||||
[this video](https://vimeo.com/820572803) or its associated code in
|
|
||||||
[github.com/jpsim/swiftlint-bazel-example](https://github.com/jpsim/swiftlint-bazel-example).
|
|
||||||
|
|
||||||
#### 2. Regex Custom Rules
|
|
||||||
|
|
||||||
You can define custom regex-based rules in your configuration file using the
|
You can define custom regex-based rules in your configuration file using the
|
||||||
following syntax:
|
following syntax:
|
||||||
|
@ -580,10 +440,8 @@ following syntax:
|
||||||
```yaml
|
```yaml
|
||||||
custom_rules:
|
custom_rules:
|
||||||
pirates_beat_ninjas: # rule identifier
|
pirates_beat_ninjas: # rule identifier
|
||||||
included:
|
included: ".*\\.swift" # regex that defines paths to include during linting. optional.
|
||||||
- ".*\\.swift" # regex that defines paths to include during linting. optional.
|
excluded: ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
|
||||||
excluded:
|
|
||||||
- ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
|
|
||||||
name: "Pirates Beat Ninjas" # rule name. optional.
|
name: "Pirates Beat Ninjas" # rule name. optional.
|
||||||
regex: "([nN]inja)" # matching pattern
|
regex: "([nN]inja)" # matching pattern
|
||||||
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
|
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
|
||||||
|
@ -605,41 +463,29 @@ You can filter the matches by providing one or more `match_kinds`, which will
|
||||||
reject matches that include syntax kinds that are not present in this list. Here
|
reject matches that include syntax kinds that are not present in this list. Here
|
||||||
are all the possible syntax kinds:
|
are all the possible syntax kinds:
|
||||||
|
|
||||||
* `argument`
|
* argument
|
||||||
* `attribute.builtin`
|
* attribute.builtin
|
||||||
* `attribute.id`
|
* attribute.id
|
||||||
* `buildconfig.id`
|
* buildconfig.id
|
||||||
* `buildconfig.keyword`
|
* buildconfig.keyword
|
||||||
* `comment`
|
* comment
|
||||||
* `comment.mark`
|
* comment.mark
|
||||||
* `comment.url`
|
* comment.url
|
||||||
* `doccomment`
|
* doccomment
|
||||||
* `doccomment.field`
|
* doccomment.field
|
||||||
* `identifier`
|
* identifier
|
||||||
* `keyword`
|
* keyword
|
||||||
* `number`
|
* number
|
||||||
* `objectliteral`
|
* objectliteral
|
||||||
* `parameter`
|
* parameter
|
||||||
* `placeholder`
|
* placeholder
|
||||||
* `string`
|
* string
|
||||||
* `string_interpolation_anchor`
|
* string_interpolation_anchor
|
||||||
* `typeidentifier`
|
* typeidentifier
|
||||||
|
|
||||||
All syntax kinds used in a snippet of Swift code can be extracted asking
|
|
||||||
[SourceKitten](https://github.com/jpsim/SourceKitten). For example,
|
|
||||||
`sourcekitten syntax --text "struct S {}"` delivers
|
|
||||||
|
|
||||||
* `source.lang.swift.syntaxtype.keyword` for the `struct` keyword and
|
|
||||||
* `source.lang.swift.syntaxtype.identifier` for its name `S`
|
|
||||||
|
|
||||||
which match to `keyword` and `identifier` in the above list.
|
|
||||||
|
|
||||||
If using custom rules in combination with `only_rules`, make sure to add
|
If using custom rules in combination with `only_rules`, make sure to add
|
||||||
`custom_rules` as an item under `only_rules`.
|
`custom_rules` as an item under `only_rules`.
|
||||||
|
|
||||||
Unlike Swift custom rules, you can use official SwiftLint builds
|
|
||||||
(e.g. from Homebrew) to run regex custom rules.
|
|
||||||
|
|
||||||
### Auto-correct
|
### Auto-correct
|
||||||
|
|
||||||
SwiftLint can automatically correct certain violations. Files on disk are
|
SwiftLint can automatically correct certain violations. Files on disk are
|
||||||
|
@ -652,9 +498,9 @@ Standard linting is disabled while correcting because of the high likelihood of
|
||||||
violations (or their offsets) being incorrect after modifying a file while
|
violations (or their offsets) being incorrect after modifying a file while
|
||||||
applying corrections.
|
applying corrections.
|
||||||
|
|
||||||
### Analyze
|
### Analyze (experimental)
|
||||||
|
|
||||||
The `swiftlint analyze` command can lint Swift files using the
|
The _experimental_ `swiftlint analyze` command can lint Swift files using the
|
||||||
full type-checked AST. The compiler log path containing the clean `swiftc` build
|
full type-checked AST. The compiler log path containing the clean `swiftc` build
|
||||||
command invocation (incremental builds will fail) must be passed to `analyze`
|
command invocation (incremental builds will fail) must be passed to `analyze`
|
||||||
via the `--compiler-log-path` flag.
|
via the `--compiler-log-path` flag.
|
||||||
|
@ -666,7 +512,9 @@ This can be obtained by
|
||||||
2. Running `xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log`
|
2. Running `xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log`
|
||||||
3. Running `swiftlint analyze --compiler-log-path xcodebuild.log`
|
3. Running `swiftlint analyze --compiler-log-path xcodebuild.log`
|
||||||
|
|
||||||
Analyzer rules tend to be considerably slower than lint rules.
|
This command and related code in SwiftLint is subject to substantial changes at
|
||||||
|
any time while this feature is marked as experimental. Analyzer rules also tend
|
||||||
|
to be considerably slower than lint rules.
|
||||||
|
|
||||||
## Using Multiple Configuration Files
|
## Using Multiple Configuration Files
|
||||||
|
|
||||||
|
@ -676,7 +524,7 @@ just as a single configuration file would get applied.
|
||||||
|
|
||||||
There are quite a lot of use cases where using multiple configuration files could be helpful:
|
There are quite a lot of use cases where using multiple configuration files could be helpful:
|
||||||
|
|
||||||
For instance, one could use a team-wide shared SwiftLint configuration while allowing overrides
|
For instance, one could use a team-wide shared SwiftLint configuration while allowing overrrides
|
||||||
in each project via a child configuration file.
|
in each project via a child configuration file.
|
||||||
|
|
||||||
Team-Wide Configuration:
|
Team-Wide Configuration:
|
||||||
|
@ -775,7 +623,7 @@ specifications of nested configurations are getting ignored because there's no s
|
||||||
If one (or more) SwiftLint file(s) are explicitly specified via the `--config` parameter,
|
If one (or more) SwiftLint file(s) are explicitly specified via the `--config` parameter,
|
||||||
that configuration will be treated as an override, no matter whether there exist
|
that configuration will be treated as an override, no matter whether there exist
|
||||||
other `.swiftlint.yml` files somewhere within the directory. **So if you want to use
|
other `.swiftlint.yml` files somewhere within the directory. **So if you want to use
|
||||||
nested configurations, you can't use the `--config` parameter.**
|
use nested configurations, you can't use the `-- config` parameter.**
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [Kodeco's Swift 代码风格指南](https://github.com/kodecocodes/swift-style-guide)为基础。
|
SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [Ray Wenderlich's Swift 代码风格指南](https://github.com/raywenderlich/swift-style-guide)为基础。
|
||||||
|
|
||||||
SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。
|
SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。
|
||||||
|
|
||||||
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
|
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
|
||||||
[](https://codecov.io/github/realm/SwiftLint?branch=main)
|
[](https://codecov.io/github/realm/SwiftLint?branch=master)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区
|
||||||
|
|
||||||
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
|
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
|
||||||
|
|
||||||
你也可以检视 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules) 目录来查看它们的实现。
|
你也可以检视 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules) 目录来查看它们的实现。
|
||||||
|
|
||||||
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。
|
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。
|
||||||
|
|
||||||
|
|
56
README_KR.md
56
README_KR.md
|
@ -1,11 +1,11 @@
|
||||||
# SwiftLint
|
# SwiftLint
|
||||||
|
|
||||||
SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [Kodeco 스위프트 스타일 가이드](https://github.com/kodecocodes/swift-style-guide)에 대략적인 기반을 두고 있습니다.
|
SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [Ray Wenderlich 스위프트 스타일 가이드](https://github.com/raywenderlich/swift-style-guide)에 대략적인 기반을 두고 있습니다.
|
||||||
|
|
||||||
SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다.
|
SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다.
|
||||||
|
|
||||||
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
|
[](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
|
||||||
[](https://codecov.io/github/realm/SwiftLint?branch=main)
|
[](https://codecov.io/github/realm/SwiftLint?branch=master)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -27,11 +27,11 @@ Podfile에 아래 라인을 추가하기만 하면 됩니다.
|
||||||
pod 'SwiftLint'
|
pod 'SwiftLint'
|
||||||
```
|
```
|
||||||
|
|
||||||
이를 실행하면 다음번 `pod install` 실행 시 SwiftLint 바이너리 및 `Pods/`에 있는 디펜던시들을 다운로드하고, Script Build Phases에서 `${PODS_ROOT}/SwiftLint/swiftlint` 명령을 사용할 수 있게 됩니다.
|
이를 실행하면 다음번 `pod install` 실행시 SwiftLint 바이너리 및 `Pods/`에 있는 디펜던시들을 다운로드하고, Script Build Phases에서 `${PODS_ROOT}/SwiftLint/swiftlint` 명령을 사용할 수 있게 됩니다.
|
||||||
|
|
||||||
CocoaPods를 사용하면 최신 버전 외에도 SwiftLint의 특정 버전을 설치할 수 있기 때문에 이 방법을 권장합니다. (Homebrew는 최신 버전만 설치 가능)
|
CocoaPods를 사용하면 최신 버전 외에도 SwiftLint의 특정 버전을 설치할 수 있기 때문에 이 방법을 권장합니다. (Homebrew는 최신 버전만 설치 가능)
|
||||||
|
|
||||||
이렇게 했을 때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉터리에 추가되기 때문에, git 등의 SCM에 이런 디렉터리들을 체크인하는 것은 권장하지 않습니다.
|
이렇게 했을때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉토리에 추가되기 때문에, git 등의 SCM에 이런 디렉토리들을 체크인하는 것은 권장하지 않습니다.
|
||||||
|
|
||||||
### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우:
|
### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우:
|
||||||
```
|
```
|
||||||
|
@ -56,7 +56,7 @@ $ mint install realm/SwiftLint
|
||||||
|
|
||||||
### Xcode
|
### Xcode
|
||||||
|
|
||||||
SwiftLint를 Xcode 프로젝트에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. 프로젝트의 파일 내비게이터에서 타겟 앱을 선택 후 "Build Phases" 탭으로 이동합니다. + 버튼을 클릭한 후 "Run Script Phase"를 선택합니다. 그 후 아래 스크립트를 추가하기만 하면 됩니다.
|
SwiftLint를 Xcode 스킴에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. "Run Script Phase"를 새로 만들고 아래 스크립트를 추가하기만 하면 됩니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
if which swiftlint >/dev/null; then
|
if which swiftlint >/dev/null; then
|
||||||
|
@ -68,34 +68,6 @@ fi
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
만약, 애플 실리콘 환경에서 Homebrew를 통해 SwiftLint를 설치했다면, 아마도 다음과 같은 경고를 겪었을 것입니다.
|
|
||||||
|
|
||||||
> warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint
|
|
||||||
|
|
||||||
그 이유는, 애플 실리콘 기반 맥에서 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
|
|
||||||
|
|
||||||
if which swiftlint > /dev/null; then
|
|
||||||
swiftlint
|
|
||||||
else
|
|
||||||
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
혹은 아래와 같이 `/usr/local/bin`에 심볼릭 링크를 생성하여 실제 바이너리가 있는 곳으로 포인팅할 수 있습니다. :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint
|
|
||||||
```
|
|
||||||
|
|
||||||
당신은 SwiftLint phase를 'Compile Sources' 단계 직전으로 옮겨 컴파일 전에 에러를 빠르게 찾고 싶어 할 것입니다. 하지만, SwiftLint는 컴파일러의 구문 분석 단계를 완벽히 수행하는 유효한 Swift 코드를 실행하기 위해 설계되었습니다. 따라서, 'Compile Sources' 전에 SwiftLint를 실행하면 일부 부정확한 오류가 발생할 수도 있습니다.
|
|
||||||
|
|
||||||
만약 당신은 위반 사항(violations)을 동시에 수정하는 것을 원한다면, 스크립트에 `swiftlint` 대신 `swiftlint --fix && swiftlint`을 적어야 합니다. 이는 프로젝트의 수정 가능한 모든 위반 사항들이 수정되고 나머지 위반 사항에 대한 경고가 표시된다는 것을 의미합니다.
|
|
||||||
|
|
||||||
CocoaPods를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다.
|
CocoaPods를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -144,7 +116,7 @@ Available commands:
|
||||||
version Display the current version of SwiftLint
|
version Display the current version of SwiftLint
|
||||||
```
|
```
|
||||||
|
|
||||||
스위프트 파일이 있는 디렉터리에서 `swiftlint`를 실행합니다. 디렉터리는 재귀적으로 탐색됩니다.
|
스위프트 파일이 있는 디렉토리에서 `swiftlint`를 실행합니다. 디렉토리는 재귀적으로 탐색됩니다.
|
||||||
|
|
||||||
`lint`나 `autocorrect`를 사용할 때 여러 파일(예를 들면, [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) 플러그인에 의해 Xcode가 변경한 파일들 혹은 `git ls-files -m` 명령으로 인해 작업 트리에서 변경된 파일들)을 지정하려면 `--use-script-input-files` 옵션을 넘기고 다음 인스턴스 변수들을 설정하면 됩니다. `SCRIPT_INPUT_FILE_COUNT` and
|
`lint`나 `autocorrect`를 사용할 때 여러 파일(예를 들면, [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) 플러그인에 의해 Xcode가 변경한 파일들 혹은 `git ls-files -m` 명령으로 인해 작업 트리에서 변경된 파일들)을 지정하려면 `--use-script-input-files` 옵션을 넘기고 다음 인스턴스 변수들을 설정하면 됩니다. `SCRIPT_INPUT_FILE_COUNT` and
|
||||||
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`
|
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`
|
||||||
|
@ -171,7 +143,7 @@ SwiftLint가 어느 스위프트 툴체인을 사용할지 결정하는 순서
|
||||||
* `~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
* `~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
||||||
* `~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
* `~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
|
||||||
|
|
||||||
`sourcekitd.framework`은 위에서 선택된 경로의 `usr/lib/` 하위 디렉터리에 존재해야 합니다.
|
`sourcekitd.framework`은 위에서 선택된 경로의 `usr/lib/` 하위 디렉토리에 존재해야 합니다.
|
||||||
|
|
||||||
`TOOLCHAINS` 환경 변수에 스위프트 툴체인 버전을 식별할 수 있는 값을 리버스 DNS 형식으로 지정할 수도 있습니다.
|
`TOOLCHAINS` 환경 변수에 스위프트 툴체인 버전을 식별할 수 있는 값을 리버스 DNS 형식으로 지정할 수도 있습니다.
|
||||||
|
|
||||||
|
@ -183,9 +155,9 @@ $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
|
||||||
|
|
||||||
## 룰
|
## 룰
|
||||||
|
|
||||||
SwiftLint에는 200개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다.
|
SwiftLint에는 75개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다.
|
||||||
|
|
||||||
현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules)를 살펴보세요.
|
현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)를 살펴보세요.
|
||||||
|
|
||||||
`opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
|
`opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
|
||||||
|
|
||||||
|
@ -231,7 +203,7 @@ let noWarning3 = NSNumber() as! Int
|
||||||
|
|
||||||
### 설정
|
### 설정
|
||||||
|
|
||||||
SwiftLint가 실행될 디렉터리에 `.swiftlint.yml` 파일을 추가해서 SwiftLint를 설정할 수 있습니다. 아래 파라미터들을 설정 가능합니다.
|
SwiftLint가 실행될 디렉토리에 `.swiftlint.yml` 파일을 추가해서 SwiftLint를 설정할 수 있습니다. 아래 파라미터들을 설정 가능합니다.
|
||||||
|
|
||||||
룰 적용여부 설정:
|
룰 적용여부 설정:
|
||||||
|
|
||||||
|
@ -296,7 +268,7 @@ reporter: "xcode" # 보고 유형 (xcode, json, csv, codeclimate, checkstyle, ju
|
||||||
```yaml
|
```yaml
|
||||||
custom_rules:
|
custom_rules:
|
||||||
pirates_beat_ninjas: # 룰 식별자
|
pirates_beat_ninjas: # 룰 식별자
|
||||||
included: ".*.swift" # 린트 실행 시 포함할 경로를 정의하는 정규표현식. 선택 가능.
|
included: ".*.swift" # 린트 실행시 포함할 경로를 정의하는 정규표현식. 선택 가능.
|
||||||
name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능.
|
name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능.
|
||||||
regex: "([nN]inja)" # 패턴 매칭
|
regex: "([nN]inja)" # 패턴 매칭
|
||||||
match_kinds: # 매칭할 SyntaxKinds. 선택 가능.
|
match_kinds: # 매칭할 SyntaxKinds. 선택 가능.
|
||||||
|
@ -339,8 +311,8 @@ custom_rules:
|
||||||
|
|
||||||
SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다.
|
SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다.
|
||||||
|
|
||||||
* 디렉터리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
|
* 디렉토리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
|
||||||
* 각 파일은 자신의 디렉터리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉터리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
|
* 각 파일은 자신의 디렉토리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉토리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
|
||||||
* 중첩 구성에서 `excluded` 및 `included`는 무시됩니다.
|
* 중첩 구성에서 `excluded` 및 `included`는 무시됩니다.
|
||||||
|
|
||||||
### 자동 수정
|
### 자동 수정
|
||||||
|
|
15
Releasing.md
15
Releasing.md
|
@ -7,8 +7,17 @@ For SwiftLint contributors, follow these steps to cut a release:
|
||||||
* FabricSoftenerRule
|
* FabricSoftenerRule
|
||||||
* Top Loading
|
* Top Loading
|
||||||
* Fresh Out Of The Dryer
|
* Fresh Out Of The Dryer
|
||||||
|
1. Push new version: `make push_version "0.2.0: Tumble Dry"`
|
||||||
1. Make sure you have the latest stable Xcode version installed and
|
1. Make sure you have the latest stable Xcode version installed and
|
||||||
`xcode-select`ed
|
`xcode-select`ed.
|
||||||
1. Release new version: `make release "0.2.0: Tumble Dry"`
|
1. Create the pkg installer, framework zip, portable zip, and Linux zip:
|
||||||
1. Wait for the Docker CI job to finish then run: `make zip_linux_release`
|
`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 pkg installer, framework zip, portable zip, and Linux zip you just built
|
||||||
|
to the GitHub release binaries.
|
||||||
|
* Click "Publish release".
|
||||||
|
1. Publish to Homebrew and CocoaPods trunk: `make publish`
|
||||||
1. Celebrate. :tada:
|
1. Celebrate. :tada:
|
||||||
|
|
|
@ -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,187 +0,0 @@
|
||||||
import SourceKittenFramework
|
|
||||||
|
|
||||||
/// Struct to represent SwiftUI ViewModifiers for the purpose of finding modifiers in a substructure.
|
|
||||||
struct SwiftUIModifier {
|
|
||||||
/// Name of the modifier.
|
|
||||||
let name: String
|
|
||||||
|
|
||||||
/// List of arguments to check for in the modifier.
|
|
||||||
let arguments: [Argument]
|
|
||||||
|
|
||||||
struct Argument {
|
|
||||||
/// Name of the argument we want to find. For single unnamed arguments, use the empty string.
|
|
||||||
let name: String
|
|
||||||
|
|
||||||
/// Whether or not the argument is required. If the argument is present, value checks are enforced.
|
|
||||||
/// Allows for better handling of modifiers with default values for certain arguments where we want
|
|
||||||
/// to ensure that the default value is used.
|
|
||||||
let required: Bool
|
|
||||||
|
|
||||||
/// List of possible values for the argument. Typically should just be a list with a single element,
|
|
||||||
/// but allows for the flexibility of checking for multiple possible values. To only check for the presence
|
|
||||||
/// of the modifier and not enforce any certain values, pass an empty array. All values are parsed as
|
|
||||||
/// Strings; for other types (boolean, numeric, optional, etc) types you can check for "true", "5", "nil", etc.
|
|
||||||
let values: [String]
|
|
||||||
|
|
||||||
/// Success criteria used for matching values (prefix, suffix, substring, exact match, or none).
|
|
||||||
let matchType: MatchType
|
|
||||||
|
|
||||||
init(name: String, required: Bool = true, values: [String], matchType: MatchType = .exactMatch) {
|
|
||||||
self.name = name
|
|
||||||
self.required = required
|
|
||||||
self.values = values
|
|
||||||
self.matchType = matchType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MatchType {
|
|
||||||
case prefix, suffix, substring, exactMatch
|
|
||||||
|
|
||||||
/// Compares the parsed argument value to a target value for the given match type
|
|
||||||
/// and returns true is a match is found.
|
|
||||||
func matches(argumentValue: String, targetValue: String) -> Bool {
|
|
||||||
switch self {
|
|
||||||
case .prefix:
|
|
||||||
return argumentValue.hasPrefix(targetValue)
|
|
||||||
case .suffix:
|
|
||||||
return argumentValue.hasSuffix(targetValue)
|
|
||||||
case .substring:
|
|
||||||
return argumentValue.contains(targetValue)
|
|
||||||
case .exactMatch:
|
|
||||||
return argumentValue == targetValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extensions for recursively checking SwiftUI code for certain modifiers.
|
|
||||||
extension SourceKittenDictionary {
|
|
||||||
/// Call on a SwiftUI View to recursively check the substructure for a certain modifier with certain arguments.
|
|
||||||
/// - Parameters:
|
|
||||||
/// - modifiers: A list of `SwiftUIModifier` structs to check for in the view's substructure.
|
|
||||||
/// In most cases, this can just be a single modifier, but since some modifiers have
|
|
||||||
/// multiple versions, this enables checking for any modifier from the list.
|
|
||||||
/// - file: The SwiftLintFile object for the current file, used to extract argument values.
|
|
||||||
/// - Returns: A boolean value representing whether or not the given modifier with the specified
|
|
||||||
/// arguments appears in the view's substructure.
|
|
||||||
func hasModifier(anyOf modifiers: [SwiftUIModifier], in file: SwiftLintFile) -> Bool {
|
|
||||||
// SwiftUI ViewModifiers are treated as `call` expressions, and we make sure we can get the expression's name.
|
|
||||||
guard expressionKind == .call, let name else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any modifier from the list matches, return true.
|
|
||||||
for modifier in modifiers {
|
|
||||||
// Check for the given modifier name
|
|
||||||
guard name.hasSuffix(modifier.name) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check arguments.
|
|
||||||
var matchesArgs = true
|
|
||||||
for argument in modifier.arguments {
|
|
||||||
var foundArg = false
|
|
||||||
var argValue: String?
|
|
||||||
|
|
||||||
// Check for single unnamed argument.
|
|
||||||
if argument.name.isEmpty {
|
|
||||||
foundArg = true
|
|
||||||
argValue = getSingleUnnamedArgumentValue(in: file)
|
|
||||||
} else if let parsedArgument = enclosedArguments.first(where: { $0.name == argument.name }) {
|
|
||||||
foundArg = true
|
|
||||||
argValue = parsedArgument.getArgumentValue(in: file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If argument is not required and we didn't find it, continue.
|
|
||||||
if !foundArg && !argument.required {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, we must have found an argument with a non-nil value to continue.
|
|
||||||
guard foundArg, let argumentValue = argValue else {
|
|
||||||
matchesArgs = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Argument value can match any of the options given in the argument struct.
|
|
||||||
if argument.values.isEmpty || argument.values.contains(where: {
|
|
||||||
argument.matchType.matches(argumentValue: argumentValue, targetValue: $0)
|
|
||||||
}) {
|
|
||||||
// Found a match, continue to next argument.
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// Did not find a match, exit loop over arguments.
|
|
||||||
matchesArgs = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if all arguments matched
|
|
||||||
if matchesArgs {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively check substructure.
|
|
||||||
// SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like:
|
|
||||||
// Image("myImage").resizable().accessibility(hidden: true).frame
|
|
||||||
// --> Image("myImage").resizable().accessibility
|
|
||||||
// --> Image("myImage").resizable
|
|
||||||
// --> Image
|
|
||||||
return substructure.contains(where: { $0.hasModifier(anyOf: modifiers, in: file) })
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Sample use cases of `hasModifier` that are used in multiple rules
|
|
||||||
|
|
||||||
/// Whether or not the dictionary represents a SwiftUI View with an `accesibilityHidden(true)`
|
|
||||||
/// or `accessibility(hidden: true)` modifier.
|
|
||||||
func hasAccessibilityHiddenModifier(in file: SwiftLintFile) -> Bool {
|
|
||||||
return hasModifier(
|
|
||||||
anyOf: [
|
|
||||||
SwiftUIModifier(
|
|
||||||
name: "accessibilityHidden",
|
|
||||||
arguments: [.init(name: "", values: ["true"])]
|
|
||||||
),
|
|
||||||
SwiftUIModifier(
|
|
||||||
name: "accessibility",
|
|
||||||
arguments: [.init(name: "hidden", values: ["true"])]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
in: file
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the dictionary represents a SwiftUI View with an `accessibilityElement()` or
|
|
||||||
/// `accessibilityElement(children: .ignore)` modifier (`.ignore` is the default parameter value).
|
|
||||||
func hasAccessibilityElementChildrenIgnoreModifier(in file: SwiftLintFile) -> Bool {
|
|
||||||
return hasModifier(
|
|
||||||
anyOf: [
|
|
||||||
SwiftUIModifier(
|
|
||||||
name: "accessibilityElement",
|
|
||||||
arguments: [.init(name: "children", required: false, values: [".ignore"], matchType: .suffix)]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
in: file
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Helpers to extract argument values
|
|
||||||
|
|
||||||
/// Helper to get the value of an argument.
|
|
||||||
func getArgumentValue(in file: SwiftLintFile) -> String? {
|
|
||||||
guard expressionKind == .argument, let bodyByteRange else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.stringView.substringWithByteRange(bodyByteRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to get the value of a single unnamed argument to a function call.
|
|
||||||
func getSingleUnnamedArgumentValue(in file: SwiftLintFile) -> String? {
|
|
||||||
guard expressionKind == .call, let bodyByteRange else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return file.stringView.substringWithByteRange(bodyByteRange)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,62 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct BlockBasedKVORule: SwiftSyntaxRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "block_based_kvo",
|
|
||||||
name: "Block Based KVO",
|
|
||||||
description: "Prefer the new block based KVO API with keypaths when using Swift 3.2 or later",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example(#"""
|
|
||||||
let observer = foo.observe(\.value, options: [.new]) { (foo, change) in
|
|
||||||
print(change.newValue)
|
|
||||||
}
|
|
||||||
"""#)
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
class Foo: NSObject {
|
|
||||||
override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,
|
|
||||||
change: [NSKeyValueChangeKey : Any]?,
|
|
||||||
context: UnsafeMutableRawPointer?) {}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
class Foo: NSObject {
|
|
||||||
override ↓func observeValue(forKeyPath keyPath: String?, of object: Any?,
|
|
||||||
change: Dictionary<NSKeyValueChangeKey, Any>?,
|
|
||||||
context: UnsafeMutableRawPointer?) {}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension BlockBasedKVORule {
|
|
||||||
private final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: FunctionDeclSyntax) {
|
|
||||||
guard node.modifiers.containsOverride,
|
|
||||||
case let parameterList = node.signature.input.parameterList,
|
|
||||||
parameterList.count == 4,
|
|
||||||
node.identifier.text == "observeValue",
|
|
||||||
parameterList.map(\.firstName.text) == ["forKeyPath", "of", "change", "context"]
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let types = parameterList
|
|
||||||
.map { $0.type.trimmedDescription.replacingOccurrences(of: " ", with: "") }
|
|
||||||
let firstTypes = ["String?", "Any?", "[NSKeyValueChangeKey:Any]?", "UnsafeMutableRawPointer?"]
|
|
||||||
let secondTypes = ["String?", "Any?", "Dictionary<NSKeyValueChangeKey,Any>?", "UnsafeMutableRawPointer?"]
|
|
||||||
if types == firstTypes || types == secondTypes {
|
|
||||||
violations.append(node.funcKeyword.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,58 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct DiscouragedObjectLiteralRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
|
|
||||||
var configuration = DiscouragedObjectLiteralConfiguration()
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "discouraged_object_literal",
|
|
||||||
name: "Discouraged Object Literal",
|
|
||||||
description: "Prefer initializers over object literals",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
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: [
|
|
||||||
Example("let image = ↓#imageLiteral(resourceName: \"image.jpg\")"),
|
|
||||||
Example("let color = ↓#colorLiteral(red: 0.9607843161, green: 0.7058823705, blue: 0.200000003, alpha: 1)")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(configuration: configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DiscouragedObjectLiteralRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
private let configuration: ConfigurationType
|
|
||||||
|
|
||||||
init(configuration: ConfigurationType) {
|
|
||||||
self.configuration = configuration
|
|
||||||
super.init(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: MacroExpansionExprSyntax) {
|
|
||||||
guard
|
|
||||||
case let .identifier(identifierText) = node.macro.tokenKind,
|
|
||||||
["colorLiteral", "imageLiteral"].contains(identifierText)
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !configuration.imageLiteral && identifierText == "imageLiteral" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !configuration.colorLiteral && identifierText == "colorLiteral" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct DiscouragedOptionalBooleanRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "discouraged_optional_boolean",
|
|
||||||
name: "Discouraged Optional Boolean",
|
|
||||||
description: "Prefer non-optional booleans over optional booleans",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: DiscouragedOptionalBooleanRuleExamples.nonTriggeringExamples,
|
|
||||||
triggeringExamples: DiscouragedOptionalBooleanRuleExamples.triggeringExamples
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension DiscouragedOptionalBooleanRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: OptionalTypeSyntax) {
|
|
||||||
if node.wrappedType.as(SimpleTypeIdentifierSyntax.self)?.typeName == "Bool" {
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: OptionalChainingExprSyntax) {
|
|
||||||
if node.expression.as(IdentifierExprSyntax.self)?.identifier.text == "Bool" {
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: FunctionCallExprSyntax) {
|
|
||||||
guard
|
|
||||||
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
|
|
||||||
let singleArgument = node.argumentList.onlyElement,
|
|
||||||
singleArgument.expression.is(BooleanLiteralExprSyntax.self),
|
|
||||||
let base = calledExpression.base?.as(IdentifierExprSyntax.self),
|
|
||||||
base.identifier.text == "Optional",
|
|
||||||
calledExpression.name.text == "some"
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import SourceKittenFramework
|
|
||||||
|
|
||||||
struct DuplicateImportsRule: ConfigurationProviderRule, CorrectableRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
// List of all possible import kinds
|
|
||||||
static let importKinds = [
|
|
||||||
"typealias", "struct", "class",
|
|
||||||
"enum", "protocol", "let",
|
|
||||||
"var", "func"
|
|
||||||
]
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "duplicate_imports",
|
|
||||||
name: "Duplicate Imports",
|
|
||||||
description: "Imports should be unique",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: DuplicateImportsRuleExamples.nonTriggeringExamples,
|
|
||||||
triggeringExamples: DuplicateImportsRuleExamples.triggeringExamples,
|
|
||||||
corrections: DuplicateImportsRuleExamples.corrections
|
|
||||||
)
|
|
||||||
|
|
||||||
private func rangesInConditionalCompilation(file: SwiftLintFile) -> [ByteRange] {
|
|
||||||
let contents = file.stringView
|
|
||||||
|
|
||||||
let ranges = file.syntaxMap.tokens
|
|
||||||
.filter { $0.kind == .buildconfigKeyword }
|
|
||||||
.map { $0.range }
|
|
||||||
.filter { range in
|
|
||||||
return ["#if", "#endif"].contains(contents.substringWithByteRange(range))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure that each #if has corresponding #endif
|
|
||||||
guard ranges.count.isMultiple(of: 2) else { return [] }
|
|
||||||
|
|
||||||
return stride(from: 0, to: ranges.count, by: 2).reduce(into: []) { result, rangeIndex in
|
|
||||||
result.append(ranges[rangeIndex].union(with: ranges[rangeIndex + 1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func buildImportLineSlicesByImportSubpath(
|
|
||||||
importLines: [Line]
|
|
||||||
) -> [ImportSubpath: [ImportLineSlice]] {
|
|
||||||
var importLineSlices = [ImportSubpath: [ImportLineSlice]]()
|
|
||||||
|
|
||||||
importLines.forEach { importLine in
|
|
||||||
importLine.importSlices.forEach { slice in
|
|
||||||
importLineSlices[slice.subpath, default: []].append(
|
|
||||||
ImportLineSlice(
|
|
||||||
slice: slice,
|
|
||||||
line: importLine
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return importLineSlices
|
|
||||||
}
|
|
||||||
|
|
||||||
private func findDuplicateImports(
|
|
||||||
file: SwiftLintFile,
|
|
||||||
importLineSlicesGroupedBySubpath: [[ImportLineSlice]]
|
|
||||||
) -> [DuplicateImport] {
|
|
||||||
typealias ImportLocation = Int
|
|
||||||
|
|
||||||
var duplicateImportsByLocation = [ImportLocation: DuplicateImport]()
|
|
||||||
|
|
||||||
importLineSlicesGroupedBySubpath.forEach { linesImportingSubpath in
|
|
||||||
guard linesImportingSubpath.count > 1 else { return }
|
|
||||||
guard let primaryImportIndex = linesImportingSubpath.firstIndex(where: {
|
|
||||||
$0.slice.type == .complete
|
|
||||||
}) else { return }
|
|
||||||
|
|
||||||
linesImportingSubpath.enumerated().forEach { index, importedLine in
|
|
||||||
guard index != primaryImportIndex else { return }
|
|
||||||
let location = Location(
|
|
||||||
file: file,
|
|
||||||
characterOffset: importedLine.line.range.location
|
|
||||||
)
|
|
||||||
duplicateImportsByLocation[importedLine.line.range.location] = DuplicateImport(
|
|
||||||
location: location,
|
|
||||||
range: importedLine.line.range
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(duplicateImportsByLocation.values)
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct DuplicateImport {
|
|
||||||
let location: Location
|
|
||||||
var range: NSRange
|
|
||||||
}
|
|
||||||
|
|
||||||
private func duplicateImports(file: SwiftLintFile) -> [DuplicateImport] {
|
|
||||||
let contents = file.stringView
|
|
||||||
|
|
||||||
let ignoredRanges = self.rangesInConditionalCompilation(file: file)
|
|
||||||
|
|
||||||
let importKinds = Self.importKinds.joined(separator: "|")
|
|
||||||
|
|
||||||
// Grammar of import declaration
|
|
||||||
// attributes(optional) import import-kind(optional) import-path
|
|
||||||
let regex = "^([a-zA-Z@_]+\\s)?import(\\s(\(importKinds)))?\\s+[a-zA-Z0-9._]+$"
|
|
||||||
let importRanges = file.match(pattern: regex)
|
|
||||||
.filter { $0.1.allSatisfy { [.keyword, .identifier, .attributeBuiltin].contains($0) } }
|
|
||||||
.compactMap { contents.NSRangeToByteRange(start: $0.0.location, length: $0.0.length) }
|
|
||||||
.filter { importRange -> Bool in
|
|
||||||
return !importRange.intersects(ignoredRanges)
|
|
||||||
}
|
|
||||||
|
|
||||||
let lines = file.lines
|
|
||||||
|
|
||||||
let importLines: [Line] = importRanges.compactMap { range in
|
|
||||||
guard let line = contents.lineAndCharacter(forByteOffset: range.location)?.line
|
|
||||||
else { return nil }
|
|
||||||
return lines[line - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
let importLineSlices = buildImportLineSlicesByImportSubpath(importLines: importLines)
|
|
||||||
|
|
||||||
let duplicateImports = findDuplicateImports(
|
|
||||||
file: file,
|
|
||||||
importLineSlicesGroupedBySubpath: Array(importLineSlices.values)
|
|
||||||
)
|
|
||||||
|
|
||||||
return duplicateImports.sorted(by: {
|
|
||||||
$0.range.lowerBound < $1.range.lowerBound
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
|
||||||
return duplicateImports(file: file).map { duplicateImport in
|
|
||||||
StyleViolation(
|
|
||||||
ruleDescription: Self.description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: duplicateImport.location
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func correct(file: SwiftLintFile) -> [Correction] {
|
|
||||||
let duplicateImports = duplicateImports(file: file).reversed().filter {
|
|
||||||
file.ruleEnabled(violatingRange: $0.range, for: self) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let violatingRanges = duplicateImports.map(\.range)
|
|
||||||
let correctedFileContents = violatingRanges.reduce(file.stringView.nsString) { contents, range in
|
|
||||||
contents.replacingCharacters(
|
|
||||||
in: range,
|
|
||||||
with: ""
|
|
||||||
).bridge()
|
|
||||||
}
|
|
||||||
|
|
||||||
file.write(correctedFileContents.bridge())
|
|
||||||
|
|
||||||
return duplicateImports.map { duplicateImport in
|
|
||||||
Correction(
|
|
||||||
ruleDescription: Self.description,
|
|
||||||
location: duplicateImport.location
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private typealias ImportSubpath = ArraySlice<String>
|
|
||||||
|
|
||||||
private struct ImportSlice {
|
|
||||||
enum ImportSliceType {
|
|
||||||
/// For "import A.B.C" parent subpaths are ["A", "B"] and ["A"]
|
|
||||||
case parent
|
|
||||||
|
|
||||||
/// For "import A.B.C" complete subpath is ["A", "B", "C"]
|
|
||||||
case complete
|
|
||||||
}
|
|
||||||
|
|
||||||
let subpath: ImportSubpath
|
|
||||||
let type: ImportSliceType
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct ImportLineSlice {
|
|
||||||
let slice: ImportSlice
|
|
||||||
let line: Line
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Line {
|
|
||||||
/// Returns name of the module being imported.
|
|
||||||
var importIdentifier: Substring? {
|
|
||||||
return self.content.split(separator: " ").last
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For "import A.B.C" returns slices [["A", "B", "C"], ["A", "B"], ["A"]]
|
|
||||||
var importSlices: [ImportSlice] {
|
|
||||||
guard let importIdentifier else { return [] }
|
|
||||||
|
|
||||||
let importedSubpathParts = importIdentifier.split(separator: ".").map { String($0) }
|
|
||||||
guard !importedSubpathParts.isEmpty else { return [] }
|
|
||||||
|
|
||||||
return [
|
|
||||||
ImportSlice(
|
|
||||||
subpath: importedSubpathParts[0..<importedSubpathParts.count],
|
|
||||||
type: .complete
|
|
||||||
)
|
|
||||||
] + (1..<importedSubpathParts.count).map {
|
|
||||||
ImportSlice(
|
|
||||||
subpath: importedSubpathParts[0..<importedSubpathParts.count - $0],
|
|
||||||
type: .parent
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,219 +0,0 @@
|
||||||
internal struct DuplicateImportsRuleExamples {
|
|
||||||
static let nonTriggeringExamples = [
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
import B
|
|
||||||
import C
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A.B
|
|
||||||
import A.C
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
@_implementationOnly import A
|
|
||||||
@_implementationOnly import B
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
@testable import A
|
|
||||||
@testable import B
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
#if DEBUG
|
|
||||||
@testable import KsApi
|
|
||||||
#else
|
|
||||||
import KsApi
|
|
||||||
#endif
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A // module
|
|
||||||
import B // module
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
#if TEST
|
|
||||||
func test() {
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
static let triggeringExamples = Array(corrections.keys.sorted())
|
|
||||||
|
|
||||||
static let corrections: [Example: Example] = {
|
|
||||||
var corrections = [
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
import Dispatch
|
|
||||||
↓import Foundation
|
|
||||||
|
|
||||||
"""): Example(
|
|
||||||
"""
|
|
||||||
import Foundation
|
|
||||||
import Dispatch
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
↓import Foundation.NSString
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import Foundation.NSString
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
@_implementationOnly import A
|
|
||||||
↓@_implementationOnly import A
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
@_implementationOnly import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
@testable import A
|
|
||||||
↓@testable import A
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
@testable import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B.C
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A.B
|
|
||||||
↓import A.B.C
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
#if DEBUG
|
|
||||||
@testable import KsApi
|
|
||||||
#else
|
|
||||||
import KsApi
|
|
||||||
#endif
|
|
||||||
↓import A
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import A
|
|
||||||
#if DEBUG
|
|
||||||
@testable import KsApi
|
|
||||||
#else
|
|
||||||
import KsApi
|
|
||||||
#endif
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
↓import Foundation
|
|
||||||
↓import Foundation
|
|
||||||
|
|
||||||
"""): Example("""
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B.C
|
|
||||||
↓import A.B
|
|
||||||
import A
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A.B.C
|
|
||||||
↓import A.B.C.D
|
|
||||||
↓import A.B.C.E
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A.B.C
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B.C
|
|
||||||
import A
|
|
||||||
↓import A.B
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
↓import A.B
|
|
||||||
import A
|
|
||||||
↓import A.B.C
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
↓import A.B.C
|
|
||||||
↓import A.B
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true): Example("""
|
|
||||||
import A
|
|
||||||
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
DuplicateImportsRule.importKinds.map { importKind in
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
↓import \(importKind) A.Foo
|
|
||||||
|
|
||||||
""")
|
|
||||||
}.forEach {
|
|
||||||
corrections[$0] = Example(
|
|
||||||
"""
|
|
||||||
import A
|
|
||||||
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
DuplicateImportsRule.importKinds.map { importKind in
|
|
||||||
Example("""
|
|
||||||
import A
|
|
||||||
↓import \(importKind) A.B.Foo
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
}.forEach {
|
|
||||||
corrections[$0] = Example(
|
|
||||||
"""
|
|
||||||
import A
|
|
||||||
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
DuplicateImportsRule.importKinds.map { importKind in
|
|
||||||
Example("""
|
|
||||||
import A.B
|
|
||||||
↓import \(importKind) A.B.Foo
|
|
||||||
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
}.forEach {
|
|
||||||
corrections[$0] = Example(
|
|
||||||
"""
|
|
||||||
import A.B
|
|
||||||
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
return corrections
|
|
||||||
}()
|
|
||||||
}
|
|
|
@ -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,254 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
import SwiftSyntaxBuilder
|
|
||||||
|
|
||||||
struct ExplicitInitRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "explicit_init",
|
|
||||||
name: "Explicit Init",
|
|
||||||
description: "Explicitly calling .init() should be avoided",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
import Foundation
|
|
||||||
class C: NSObject {
|
|
||||||
override init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""), // super
|
|
||||||
Example("""
|
|
||||||
struct S {
|
|
||||||
let n: Int
|
|
||||||
}
|
|
||||||
extension S {
|
|
||||||
init() {
|
|
||||||
self.init(n: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""), // self
|
|
||||||
Example("""
|
|
||||||
[1].flatMap(String.init)
|
|
||||||
"""), // pass init as closure
|
|
||||||
Example("""
|
|
||||||
[String.self].map { $0.init(1) }
|
|
||||||
"""), // initialize from a metatype value
|
|
||||||
Example("""
|
|
||||||
[String.self].map { type in type.init(1) }
|
|
||||||
"""), // initialize from a metatype value
|
|
||||||
Example("""
|
|
||||||
Observable.zip(obs1, obs2, resultSelector: MyType.init).asMaybe()
|
|
||||||
"""),
|
|
||||||
Example("_ = GleanMetrics.Tabs.someType.init()"),
|
|
||||||
Example("""
|
|
||||||
Observable.zip(
|
|
||||||
obs1,
|
|
||||||
obs2,
|
|
||||||
resultSelector: MyType.init
|
|
||||||
).asMaybe()
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
[1].flatMap{String↓.init($0)}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
[String.self].map { Type in Type↓.init(1) }
|
|
||||||
"""), // Starting with capital letter assumes a type
|
|
||||||
Example("""
|
|
||||||
func foo() -> [String] {
|
|
||||||
return [1].flatMap { String↓.init($0) }
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("_ = GleanMetrics.Tabs.GroupedTabExtra↓.init()"),
|
|
||||||
Example("_ = Set<KsApi.Category>↓.init()"),
|
|
||||||
Example("""
|
|
||||||
Observable.zip(
|
|
||||||
obs1,
|
|
||||||
obs2,
|
|
||||||
resultSelector: { MyType↓.init($0, $1) }
|
|
||||||
).asMaybe()
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
let int = In🤓t↓
|
|
||||||
.init(1.0)
|
|
||||||
""", excludeFromDocumentation: true),
|
|
||||||
Example("""
|
|
||||||
let int = Int↓
|
|
||||||
|
|
||||||
|
|
||||||
.init(1.0)
|
|
||||||
""", excludeFromDocumentation: true),
|
|
||||||
Example("""
|
|
||||||
let int = Int↓
|
|
||||||
|
|
||||||
|
|
||||||
.init(1.0)
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
],
|
|
||||||
corrections: [
|
|
||||||
Example("""
|
|
||||||
[1].flatMap{String↓.init($0)}
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
[1].flatMap{String($0)}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() -> [String] {
|
|
||||||
return [1].flatMap { String↓.init($0) }
|
|
||||||
}
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
func foo() -> [String] {
|
|
||||||
return [1].flatMap { String($0) }
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
class C {
|
|
||||||
#if true
|
|
||||||
func f() {
|
|
||||||
[1].flatMap{String↓.init($0)}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
class C {
|
|
||||||
#if true
|
|
||||||
func f() {
|
|
||||||
[1].flatMap{String($0)}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
let int = Int↓
|
|
||||||
.init(1.0)
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
let int = Int(1.0)
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
let int = Int↓
|
|
||||||
|
|
||||||
|
|
||||||
.init(1.0)
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
let int = Int(1.0)
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
let int = Int↓
|
|
||||||
|
|
||||||
|
|
||||||
.init(1.0)
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
let int = Int(1.0)
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
let int = Int↓
|
|
||||||
|
|
||||||
|
|
||||||
.init(1.0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""):
|
|
||||||
Example("""
|
|
||||||
let int = Int(1.0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""),
|
|
||||||
Example("_ = GleanMetrics.Tabs.GroupedTabExtra↓.init()"):
|
|
||||||
Example("_ = GleanMetrics.Tabs.GroupedTabExtra()"),
|
|
||||||
Example("_ = Set<KsApi.Category>↓.init()"):
|
|
||||||
Example("_ = Set<KsApi.Category>()")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
|
||||||
Rewriter(
|
|
||||||
locationConverter: file.locationConverter,
|
|
||||||
disabledRegions: disabledRegions(file: file)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ExplicitInitRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: FunctionCallExprSyntax) {
|
|
||||||
guard
|
|
||||||
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
|
|
||||||
let violationPosition = calledExpression.explicitInitPosition
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(violationPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
|
|
||||||
let violationPosition = calledExpression.explicitInitPosition,
|
|
||||||
let calledBase = calledExpression.base,
|
|
||||||
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
|
||||||
else {
|
|
||||||
return super.visit(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
correctionPositions.append(violationPosition)
|
|
||||||
let newNode = node.with(\.calledExpression, calledBase.trimmed)
|
|
||||||
return super.visit(newNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension MemberAccessExprSyntax {
|
|
||||||
var explicitInitPosition: AbsolutePosition? {
|
|
||||||
if let base, base.isTypeReferenceLike, name.text == "init" {
|
|
||||||
return base.endPositionBeforeTrailingTrivia
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ExprSyntax {
|
|
||||||
/// `String` or `Nested.Type`.
|
|
||||||
var isTypeReferenceLike: Bool {
|
|
||||||
if let expr = self.as(IdentifierExprSyntax.self), expr.identifier.text.startsWithUppercase {
|
|
||||||
return true
|
|
||||||
} else if let expr = self.as(MemberAccessExprSyntax.self),
|
|
||||||
expr.description.split(separator: ".").allSatisfy(\.startsWithUppercase) {
|
|
||||||
return true
|
|
||||||
} else if let expr = self.as(SpecializeExprSyntax.self)?.expression.as(IdentifierExprSyntax.self),
|
|
||||||
expr.identifier.text.startsWithUppercase {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension StringProtocol {
|
|
||||||
var startsWithUppercase: Bool { first?.isUppercase == true }
|
|
||||||
}
|
|
|
@ -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,42 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct FallthroughRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "fallthrough",
|
|
||||||
name: "Fallthrough",
|
|
||||||
description: "Fallthrough should be avoided",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
switch foo {
|
|
||||||
case .bar, .bar2, .bar3:
|
|
||||||
something()
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
switch foo {
|
|
||||||
case .bar:
|
|
||||||
↓fallthrough
|
|
||||||
case .bar2:
|
|
||||||
something()
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension FallthroughRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: FallthroughStmtSyntax) {
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,40 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct ForceTryRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.error)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "force_try",
|
|
||||||
name: "Force Try",
|
|
||||||
description: "Force tries should be avoided",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
func a() throws {}
|
|
||||||
do {
|
|
||||||
try a()
|
|
||||||
} catch {}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
func a() throws {}
|
|
||||||
↓try! a()
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ForceTryRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: TryExprSyntax) {
|
|
||||||
if node.questionOrExclamationMark?.tokenKind == .exclamationMark {
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct ForceUnwrappingRule: OptInRule, SwiftSyntaxRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "force_unwrapping",
|
|
||||||
name: "Force Unwrapping",
|
|
||||||
description: "Force unwrapping should be avoided",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("if let url = NSURL(string: query)"),
|
|
||||||
Example("navigationController?.pushViewController(viewController, animated: true)"),
|
|
||||||
Example("let s as! Test"),
|
|
||||||
Example("try! canThrowErrors()"),
|
|
||||||
Example("let object: Any!"),
|
|
||||||
Example("@IBOutlet var constraints: [NSLayoutConstraint]!"),
|
|
||||||
Example("setEditing(!editing, animated: true)"),
|
|
||||||
Example("navigationController.setNavigationBarHidden(!navigationController." +
|
|
||||||
"navigationBarHidden, animated: true)"),
|
|
||||||
Example("if addedToPlaylist && (!self.selectedFilters.isEmpty || " +
|
|
||||||
"self.searchBar?.text?.isEmpty == false) {}"),
|
|
||||||
Example("print(\"\\(xVar)!\")"),
|
|
||||||
Example("var test = (!bar)"),
|
|
||||||
Example("var a: [Int]!"),
|
|
||||||
Example("private var myProperty: (Void -> Void)!"),
|
|
||||||
Example("func foo(_ options: [AnyHashable: Any]!) {"),
|
|
||||||
Example("func foo() -> [Int]!"),
|
|
||||||
Example("func foo() -> [AnyHashable: Any]!"),
|
|
||||||
Example("func foo() -> [Int]! { return [] }"),
|
|
||||||
Example("return self")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("let url = NSURL(string: query)↓!"),
|
|
||||||
Example("navigationController↓!.pushViewController(viewController, animated: true)"),
|
|
||||||
Example("let unwrapped = optional↓!"),
|
|
||||||
Example("return cell↓!"),
|
|
||||||
Example("let url = NSURL(string: \"http://www.google.com\")↓!"),
|
|
||||||
Example("""
|
|
||||||
let dict = ["Boooo": "👻"]
|
|
||||||
func bla() -> String {
|
|
||||||
return dict["Boooo"]↓!
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
let dict = ["Boooo": "👻"]
|
|
||||||
func bla() -> String {
|
|
||||||
return dict["Boooo"]↓!.contains("B")
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("let a = dict[\"abc\"]↓!.contains(\"B\")"),
|
|
||||||
Example("dict[\"abc\"]↓!.bar(\"B\")"),
|
|
||||||
Example("if dict[\"a\"]↓!↓!↓!↓! {}"),
|
|
||||||
Example("var foo: [Bool]! = dict[\"abc\"]↓!"),
|
|
||||||
Example("realm.objects(SwiftUTF8Object.self).filter(\"%K == %@\", \"柱нǢкƱаم👍\", utf8TestString).first↓!"),
|
|
||||||
Example("""
|
|
||||||
context("abc") {
|
|
||||||
var foo: [Bool]! = dict["abc"]↓!
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("open var computed: String { return foo.bar↓! }"),
|
|
||||||
Example("return self↓!"),
|
|
||||||
Example("[1, 3, 5, 6].first { $0.isMultiple(of: 2) }↓!"),
|
|
||||||
Example("map[\"a\"]↓!↓!")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
ForceUnwrappingVisitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class ForceUnwrappingVisitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: ForcedValueExprSyntax) {
|
|
||||||
violations.append(node.exclamationMark.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,101 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct GenericTypeNameRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
|
||||||
var configuration = NameConfiguration<Self>(minLengthWarning: 1,
|
|
||||||
minLengthError: 0,
|
|
||||||
maxLengthWarning: 20,
|
|
||||||
maxLengthError: 1000)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "generic_type_name",
|
|
||||||
name: "Generic Type Name",
|
|
||||||
description: "Generic type name should only contain alphanumeric characters, start with an " +
|
|
||||||
"uppercase character and span between 1 and 20 characters in length.",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("func foo<T>() {}\n"),
|
|
||||||
Example("func foo<T>() -> T {}\n"),
|
|
||||||
Example("func foo<T, U>(param: U) -> T {}\n"),
|
|
||||||
Example("func foo<T: Hashable, U: Rule>(param: U) -> T {}\n"),
|
|
||||||
Example("struct Foo<T> {}\n"),
|
|
||||||
Example("class Foo<T> {}\n"),
|
|
||||||
Example("enum Foo<T> {}\n"),
|
|
||||||
Example("func run(_ options: NoOptions<CommandantError<()>>) {}\n"),
|
|
||||||
Example("func foo(_ options: Set<type>) {}\n"),
|
|
||||||
Example("func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool\n"),
|
|
||||||
Example("func configureWith(data: Either<MessageThread, (project: Project, backing: Backing)>)\n"),
|
|
||||||
Example("typealias StringDictionary<T> = Dictionary<String, T>\n"),
|
|
||||||
Example("typealias BackwardTriple<T1, T2, T3> = (T3, T2, T1)\n"),
|
|
||||||
Example("typealias DictionaryOfStrings<T : Hashable> = Dictionary<T, String>\n")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("func foo<↓T_Foo>() {}\n"),
|
|
||||||
Example("func foo<T, ↓U_Foo>(param: U_Foo) -> T {}\n"),
|
|
||||||
Example("func foo<↓\(String(repeating: "T", count: 21))>() {}\n"),
|
|
||||||
Example("func foo<↓type>() {}\n"),
|
|
||||||
Example("typealias StringDictionary<↓T_Foo> = Dictionary<String, T_Foo>\n"),
|
|
||||||
Example("typealias BackwardTriple<T1, ↓T2_Bar, T3> = (T3, T2_Bar, T1)\n"),
|
|
||||||
Example("typealias DictionaryOfStrings<↓T_Foo: Hashable> = Dictionary<T_Foo, String>\n")
|
|
||||||
] + ["class", "struct", "enum"].flatMap { type -> [Example] in
|
|
||||||
return [
|
|
||||||
Example("\(type) Foo<↓T_Foo> {}\n"),
|
|
||||||
Example("\(type) Foo<T, ↓U_Foo> {}\n"),
|
|
||||||
Example("\(type) Foo<↓T_Foo, ↓U_Foo> {}\n"),
|
|
||||||
Example("\(type) Foo<↓\(String(repeating: "T", count: 21))> {}\n"),
|
|
||||||
Example("\(type) Foo<↓type> {}\n")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(configuration: configuration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension GenericTypeNameRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
private let configuration: ConfigurationType
|
|
||||||
|
|
||||||
init(configuration: ConfigurationType) {
|
|
||||||
self.configuration = configuration
|
|
||||||
super.init(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: GenericParameterSyntax) {
|
|
||||||
let name = node.name.text
|
|
||||||
guard !name.isEmpty, !configuration.shouldExclude(name: name) else { return }
|
|
||||||
|
|
||||||
if !configuration.allowedSymbolsAndAlphanumerics.isSuperset(of: CharacterSet(charactersIn: name)) {
|
|
||||||
violations.append(
|
|
||||||
ReasonedRuleViolation(
|
|
||||||
position: node.positionAfterSkippingLeadingTrivia,
|
|
||||||
reason: """
|
|
||||||
Generic type name '\(name)' should only contain alphanumeric and other allowed characters
|
|
||||||
""",
|
|
||||||
severity: .error
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if let caseCheckSeverity = configuration.validatesStartWithLowercase.severity,
|
|
||||||
!String(name[name.startIndex]).isUppercase() {
|
|
||||||
violations.append(
|
|
||||||
ReasonedRuleViolation(
|
|
||||||
position: node.positionAfterSkippingLeadingTrivia,
|
|
||||||
reason: "Generic type name '\(name)' should start with an uppercase character",
|
|
||||||
severity: caseCheckSeverity
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if let severity = configuration.severity(forLength: name.count) {
|
|
||||||
let reason = "Generic type name '\(name)' should be between \(configuration.minLengthThreshold) and " +
|
|
||||||
"\(configuration.maxLengthThreshold) characters long"
|
|
||||||
violations.append(
|
|
||||||
ReasonedRuleViolation(
|
|
||||||
position: node.positionAfterSkippingLeadingTrivia,
|
|
||||||
reason: reason,
|
|
||||||
severity: severity
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,79 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct RedundantNilCoalescingRule: OptInRule, SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "redundant_nil_coalescing",
|
|
||||||
name: "Redundant Nil Coalescing",
|
|
||||||
description: "nil coalescing operator is only evaluated if the lhs is nil" +
|
|
||||||
", coalescing operator with nil as rhs is redundant",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("var myVar: Int?; myVar ?? 0\n")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("var myVar: Int? = nil; myVar ↓?? nil\n")
|
|
||||||
],
|
|
||||||
corrections: [
|
|
||||||
Example("var myVar: Int? = nil; let foo = myVar↓ ?? nil\n"):
|
|
||||||
Example("var myVar: Int? = nil; let foo = myVar\n")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
|
||||||
Rewriter(
|
|
||||||
locationConverter: file.locationConverter,
|
|
||||||
disabledRegions: disabledRegions(file: file)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension RedundantNilCoalescingRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: TokenSyntax) {
|
|
||||||
if node.tokenKind.isNilCoalescingOperator,
|
|
||||||
node.nextToken(viewMode: .sourceAccurate)?.tokenKind == .keyword(.nil) {
|
|
||||||
violations.append(node.position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: ExprListSyntax) -> ExprListSyntax {
|
|
||||||
guard
|
|
||||||
node.count > 2,
|
|
||||||
let lastExpression = node.last,
|
|
||||||
lastExpression.is(NilLiteralExprSyntax.self),
|
|
||||||
let secondToLastExpression = node.dropLast().last?.as(BinaryOperatorExprSyntax.self),
|
|
||||||
secondToLastExpression.operatorToken.tokenKind.isNilCoalescingOperator,
|
|
||||||
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
|
||||||
else {
|
|
||||||
return super.visit(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
let newNode = node.removingLast().removingLast().with(\.trailingTrivia, [])
|
|
||||||
correctionPositions.append(newNode.endPosition)
|
|
||||||
return super.visit(newNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension TokenKind {
|
|
||||||
var isNilCoalescingOperator: Bool {
|
|
||||||
self == .binaryOperator("??")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,112 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct RedundantStringEnumValueRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "redundant_string_enum_value",
|
|
||||||
name: "Redundant String Enum Value",
|
|
||||||
description: "String enum values can be omitted when they are equal to the enumcase name",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one
|
|
||||||
case two
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
enum Numbers: Int {
|
|
||||||
case one = 1
|
|
||||||
case two = 2
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one = "ONE"
|
|
||||||
case two = "TWO"
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one = "ONE"
|
|
||||||
case two = "two"
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one, two
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one = ↓"one"
|
|
||||||
case two = ↓"two"
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one = ↓"one", two = ↓"two"
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
enum Numbers: String {
|
|
||||||
case one, two = ↓"two"
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension RedundantStringEnumValueRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: EnumDeclSyntax) {
|
|
||||||
guard node.isStringEnum else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let enumsWithExplicitValues = node.memberBlock.members
|
|
||||||
.flatMap { member -> EnumCaseElementListSyntax in
|
|
||||||
guard let enumCaseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
|
|
||||||
return EnumCaseElementListSyntax([])
|
|
||||||
}
|
|
||||||
|
|
||||||
return enumCaseDecl.elements
|
|
||||||
}
|
|
||||||
.filter { $0.rawValue != nil }
|
|
||||||
|
|
||||||
let redundantMembersPositions = enumsWithExplicitValues
|
|
||||||
.compactMap { element -> AbsolutePosition? in
|
|
||||||
guard let stringExpr = element.rawValue?.value.as(StringLiteralExprSyntax.self),
|
|
||||||
let segment = stringExpr.segments.onlyElement?.as(StringSegmentSyntax.self),
|
|
||||||
segment.content.text == element.identifier.text else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringExpr.positionAfterSkippingLeadingTrivia
|
|
||||||
}
|
|
||||||
|
|
||||||
if redundantMembersPositions.count == enumsWithExplicitValues.count {
|
|
||||||
violations.append(contentsOf: redundantMembersPositions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension EnumDeclSyntax {
|
|
||||||
var isStringEnum: Bool {
|
|
||||||
guard let inheritanceClause else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return inheritanceClause.inheritedTypeCollection.contains { elem in
|
|
||||||
elem.typeName.as(SimpleTypeIdentifierSyntax.self)?.typeName == "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct ReturnValueFromVoidFunctionRule: ConfigurationProviderRule, OptInRule, SwiftSyntaxRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "return_value_from_void_function",
|
|
||||||
name: "Return Value from Void Function",
|
|
||||||
description: "Returning values from Void functions should be avoided",
|
|
||||||
kind: .idiomatic,
|
|
||||||
minSwiftVersion: .fiveDotOne,
|
|
||||||
nonTriggeringExamples: ReturnValueFromVoidFunctionRuleExamples.nonTriggeringExamples,
|
|
||||||
triggeringExamples: ReturnValueFromVoidFunctionRuleExamples.triggeringExamples
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
ReturnValueFromVoidFunctionVisitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class ReturnValueFromVoidFunctionVisitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: ReturnStmtSyntax) {
|
|
||||||
if node.expression != nil,
|
|
||||||
let functionNode = Syntax(node).enclosingFunction(),
|
|
||||||
functionNode.returnsVoid {
|
|
||||||
violations.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Syntax {
|
|
||||||
func enclosingFunction() -> FunctionDeclSyntax? {
|
|
||||||
if let node = self.as(FunctionDeclSyntax.self) {
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is(ClosureExprSyntax.self) || self.is(VariableDeclSyntax.self) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent?.enclosingFunction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension FunctionDeclSyntax {
|
|
||||||
var returnsVoid: Bool {
|
|
||||||
if let type = signature.output?.returnType.as(SimpleTypeIdentifierSyntax.self) {
|
|
||||||
return type.name.text == "Void"
|
|
||||||
}
|
|
||||||
|
|
||||||
return signature.output?.returnType == nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,267 +0,0 @@
|
||||||
internal struct ReturnValueFromVoidFunctionRuleExamples {
|
|
||||||
static let nonTriggeringExamples = [
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
return /* a comment */
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() -> Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() -> Void {
|
|
||||||
if condition {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
return;
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("func test() {}"),
|
|
||||||
Example("""
|
|
||||||
init?() {
|
|
||||||
guard condition else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
init?(arg: String?) {
|
|
||||||
guard arg != nil else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
guard condition else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() -> Result<String, Error> {
|
|
||||||
func other() {}
|
|
||||||
func otherVoid() -> Void {}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() -> Int? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
if bar {
|
|
||||||
print("")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let foo = [1, 2, 3].filter { return true }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
guard foo else {
|
|
||||||
bar()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func spec() {
|
|
||||||
var foo: Int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example(#"""
|
|
||||||
final class SearchMessagesDataSource: ValueCellDataSource {
|
|
||||||
internal enum Section: Int {
|
|
||||||
case emptyState
|
|
||||||
case messageThreads
|
|
||||||
}
|
|
||||||
|
|
||||||
internal func load(messageThreads: [MessageThread]) {
|
|
||||||
self.set(
|
|
||||||
values: messageThreads,
|
|
||||||
cellClass: MessageThreadCell.self,
|
|
||||||
inSection: Section.messageThreads.rawValue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal func emptyState(isVisible: Bool) {
|
|
||||||
self.set(
|
|
||||||
cellIdentifiers: isVisible ? ["SearchMessagesEmptyState"] : [],
|
|
||||||
inSection: Section.emptyState.rawValue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) {
|
|
||||||
switch (cell, value) {
|
|
||||||
case let (cell as MessageThreadCell, value as MessageThread):
|
|
||||||
cell.configureWith(value: value)
|
|
||||||
case (is StaticTableViewCell, is Void):
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
assertionFailure("Unrecognized combo: \(cell), \(value).")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""#, excludeFromDocumentation: true)
|
|
||||||
]
|
|
||||||
|
|
||||||
static let triggeringExamples = [
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
↓return bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
↓return self.bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() -> Void {
|
|
||||||
↓return bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() -> Void {
|
|
||||||
↓return /* comment */ bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
↓return
|
|
||||||
self.bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func foo() {
|
|
||||||
variable += 1
|
|
||||||
↓return
|
|
||||||
variable += 1
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func initThing() {
|
|
||||||
guard foo else {
|
|
||||||
↓return print("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
// Leading comment
|
|
||||||
func test() {
|
|
||||||
guard condition else {
|
|
||||||
↓return assertionfailure("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() -> Result<String, Error> {
|
|
||||||
func other() {
|
|
||||||
guard false else {
|
|
||||||
↓return assertionfailure("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func otherVoid() -> Void {}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
guard conditionIsTrue else {
|
|
||||||
sideEffects()
|
|
||||||
return // comment
|
|
||||||
}
|
|
||||||
guard otherCondition else {
|
|
||||||
↓return assertionfailure("")
|
|
||||||
}
|
|
||||||
differentSideEffect()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
guard otherCondition else {
|
|
||||||
↓return assertionfailure(""); // comment
|
|
||||||
}
|
|
||||||
differentSideEffect()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
if x {
|
|
||||||
↓return foo()
|
|
||||||
}
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
switch x {
|
|
||||||
case .a:
|
|
||||||
↓return foo() // return to skip baz()
|
|
||||||
case .b:
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
baz()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
if check {
|
|
||||||
if otherCheck {
|
|
||||||
↓return foo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
↓return foo()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
↓return foo({
|
|
||||||
return bar()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
guard x else {
|
|
||||||
↓return foo()
|
|
||||||
}
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func test() {
|
|
||||||
let closure: () -> () = {
|
|
||||||
return assert()
|
|
||||||
}
|
|
||||||
if check {
|
|
||||||
if otherCheck {
|
|
||||||
return // comments are fine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
↓return foo()
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct ShorthandOptionalBindingRule: OptInRule, SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static var description = RuleDescription(
|
|
||||||
identifier: "shorthand_optional_binding",
|
|
||||||
name: "Shorthand Optional Binding",
|
|
||||||
description: "Use shorthand syntax for optional binding",
|
|
||||||
kind: .idiomatic,
|
|
||||||
minSwiftVersion: .fiveDotSeven,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
if let i {}
|
|
||||||
if let i = a {}
|
|
||||||
guard let i = f() else {}
|
|
||||||
if var i = i() {}
|
|
||||||
if let i = i as? Foo {}
|
|
||||||
guard let `self` = self else {}
|
|
||||||
while var i { i = nil }
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if let i,
|
|
||||||
var i = a,
|
|
||||||
j > 0 {}
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
if ↓let i = i {}
|
|
||||||
if ↓let self = self {}
|
|
||||||
if ↓var `self` = `self` {}
|
|
||||||
if i > 0, ↓let j = j {}
|
|
||||||
if ↓let i = i, ↓var j = j {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if ↓let i = i,
|
|
||||||
↓var j = j,
|
|
||||||
j > 0 {}
|
|
||||||
""", excludeFromDocumentation: true),
|
|
||||||
Example("""
|
|
||||||
guard ↓let i = i else {}
|
|
||||||
guard ↓let self = self else {}
|
|
||||||
guard ↓var `self` = `self` else {}
|
|
||||||
guard i > 0, ↓let j = j else {}
|
|
||||||
guard ↓let i = i, ↓var j = j else {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
while ↓var i = i { i = nil }
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
corrections: [
|
|
||||||
Example("""
|
|
||||||
if ↓let i = i {}
|
|
||||||
"""): Example("""
|
|
||||||
if let i {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if ↓let self = self {}
|
|
||||||
"""): Example("""
|
|
||||||
if let self {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if ↓var `self` = `self` {}
|
|
||||||
"""): Example("""
|
|
||||||
if var `self` {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
guard ↓let i = i, ↓var j = j , ↓let k =k else {}
|
|
||||||
"""): Example("""
|
|
||||||
guard let i, var j , let k else {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
while j > 0, ↓var i = i { i = nil }
|
|
||||||
"""): Example("""
|
|
||||||
while j > 0, var i { i = nil }
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
deprecatedAliases: ["if_let_shadowing"]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
|
||||||
Rewriter(
|
|
||||||
locationConverter: file.locationConverter,
|
|
||||||
disabledRegions: disabledRegions(file: file)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: OptionalBindingConditionSyntax) {
|
|
||||||
if node.isShadowingOptionalBinding {
|
|
||||||
violations.append(node.bindingKeyword.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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: OptionalBindingConditionSyntax) -> OptionalBindingConditionSyntax {
|
|
||||||
guard
|
|
||||||
node.isShadowingOptionalBinding,
|
|
||||||
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
|
||||||
else {
|
|
||||||
return super.visit(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
let newNode = node
|
|
||||||
.with(\.initializer, nil)
|
|
||||||
.with(\.pattern, node.pattern.with(\.trailingTrivia, node.trailingTrivia))
|
|
||||||
return super.visit(newNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension OptionalBindingConditionSyntax {
|
|
||||||
var isShadowingOptionalBinding: Bool {
|
|
||||||
if let id = pattern.as(IdentifierPatternSyntax.self),
|
|
||||||
let value = initializer?.value.as(IdentifierExprSyntax.self),
|
|
||||||
id.identifier.text == value.identifier.text {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,339 +0,0 @@
|
||||||
import Foundation
|
|
||||||
import SourceKittenFramework
|
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct SyntacticSugarRule: CorrectableRule, ConfigurationProviderRule, SourceKitFreeRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "syntactic_sugar",
|
|
||||||
name: "Syntactic Sugar",
|
|
||||||
description: "Shorthand syntactic sugar should be used, i.e. [Int] instead of Array<Int>.",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: SyntacticSugarRuleExamples.nonTriggering,
|
|
||||||
triggeringExamples: SyntacticSugarRuleExamples.triggering,
|
|
||||||
corrections: SyntacticSugarRuleExamples.corrections
|
|
||||||
)
|
|
||||||
|
|
||||||
func validate(file: SwiftLintFile) -> [StyleViolation] {
|
|
||||||
let visitor = SyntacticSugarRuleVisitor(viewMode: .sourceAccurate)
|
|
||||||
return visitor.walk(file: file) { visitor in
|
|
||||||
flattenViolations(visitor.violations)
|
|
||||||
}.map { violation in
|
|
||||||
return StyleViolation(ruleDescription: Self.description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: Location(file: file, byteOffset: ByteCount(violation.position)),
|
|
||||||
reason: violation.type.violationReason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func flattenViolations(_ violations: [SyntacticSugarRuleViolation]) -> [SyntacticSugarRuleViolation] {
|
|
||||||
return violations.flatMap { [$0] + flattenViolations($0.children) }
|
|
||||||
}
|
|
||||||
|
|
||||||
func correct(file: SwiftLintFile) -> [Correction] {
|
|
||||||
let visitor = SyntacticSugarRuleVisitor(viewMode: .sourceAccurate)
|
|
||||||
return visitor.walk(file: file) { visitor in
|
|
||||||
var context = CorrectingContext(rule: self, file: file, contents: file.contents)
|
|
||||||
context.correctViolations(visitor.violations)
|
|
||||||
|
|
||||||
file.write(context.contents)
|
|
||||||
|
|
||||||
return context.corrections
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
|
|
||||||
private enum SugaredType: String {
|
|
||||||
case optional = "Optional"
|
|
||||||
case array = "Array"
|
|
||||||
case dictionary = "Dictionary"
|
|
||||||
|
|
||||||
init?(typeName: String) {
|
|
||||||
var typeName = typeName
|
|
||||||
if typeName.hasPrefix("Swift.") {
|
|
||||||
typeName.removeFirst("Swift.".count)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.init(rawValue: typeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sugaredExample: String {
|
|
||||||
switch self {
|
|
||||||
case .optional:
|
|
||||||
return "Int?"
|
|
||||||
case .array:
|
|
||||||
return "[Int]"
|
|
||||||
case .dictionary:
|
|
||||||
return "[String: Int]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var desugaredExample: String {
|
|
||||||
switch self {
|
|
||||||
case .optional, .array:
|
|
||||||
return "\(rawValue)<Int>"
|
|
||||||
case .dictionary:
|
|
||||||
return "\(rawValue)<String, Int>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var violationReason: String {
|
|
||||||
"Shorthand syntactic sugar should be used, i.e. \(sugaredExample) instead of \(desugaredExample)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct SyntacticSugarRuleViolation {
|
|
||||||
struct Correction {
|
|
||||||
let typeStart: AbsolutePosition
|
|
||||||
let correction: CorrectionType
|
|
||||||
|
|
||||||
let leftStart: AbsolutePosition
|
|
||||||
let leftEnd: AbsolutePosition
|
|
||||||
|
|
||||||
let rightStart: AbsolutePosition
|
|
||||||
let rightEnd: AbsolutePosition
|
|
||||||
}
|
|
||||||
enum CorrectionType {
|
|
||||||
case optional
|
|
||||||
case dictionary(commaStart: AbsolutePosition, commaEnd: AbsolutePosition)
|
|
||||||
case array
|
|
||||||
}
|
|
||||||
|
|
||||||
let position: AbsolutePosition
|
|
||||||
let type: SugaredType
|
|
||||||
|
|
||||||
let correction: Correction
|
|
||||||
|
|
||||||
var children: [SyntacticSugarRuleViolation] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class SyntacticSugarRuleVisitor: SyntaxVisitor {
|
|
||||||
var violations: [SyntacticSugarRuleViolation] = []
|
|
||||||
|
|
||||||
override func visitPost(_ node: TypeAnnotationSyntax) {
|
|
||||||
// let x: ↓Swift.Optional<String>
|
|
||||||
// let x: ↓Optional<String>
|
|
||||||
if let violation = violation(in: node.type) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: FunctionParameterSyntax) {
|
|
||||||
// func x(a: ↓Array<Int>, b: Int) -> [Int: Any]
|
|
||||||
if let violation = violation(in: node.type) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: ReturnClauseSyntax) {
|
|
||||||
// func x(a: [Int], b: Int) -> ↓Dictionary<Int, String>
|
|
||||||
if let violation = violation(in: node.returnType) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: TypeInitializerClauseSyntax) {
|
|
||||||
// typealias Document = ↓Dictionary<String, AnyBSON?>
|
|
||||||
if let violation = violation(in: node.value) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: AttributedTypeSyntax) {
|
|
||||||
// func x(_ y: inout ↓Array<T>)
|
|
||||||
if let violation = violation(in: node.baseType) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: SameTypeRequirementSyntax) {
|
|
||||||
// @_specialize(where S == ↓Array<Character>)
|
|
||||||
if let violation = violation(in: node.leftTypeIdentifier) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
if let violation = violation(in: node.rightTypeIdentifier) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: SpecializeExprSyntax) {
|
|
||||||
// let x = ↓Array<String>.array(of: object)
|
|
||||||
// Skip checks for 'self' or \T Dictionary<Key, Value>.self
|
|
||||||
if let parent = node.parent?.as(MemberAccessExprSyntax.self),
|
|
||||||
let lastToken = Array(parent.tokens(viewMode: .sourceAccurate)).last?.tokenKind,
|
|
||||||
[.keyword(.self), .identifier("Type"), .identifier("none"), .identifier("Index")].contains(lastToken) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let typeName = node.expression.trimmedDescription
|
|
||||||
|
|
||||||
if SugaredType(typeName: typeName) != nil {
|
|
||||||
if let violation = violation(from: node) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no type, check all inner generics like in the case of 'Box<Array<T>>'
|
|
||||||
node.genericArgumentClause.arguments
|
|
||||||
.lazy
|
|
||||||
.compactMap { self.violation(in: $0.argumentType) }
|
|
||||||
.first
|
|
||||||
.map { violations.append($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: TypeExprSyntax) {
|
|
||||||
if let violation = violation(in: node.type) {
|
|
||||||
violations.append(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func violation(in typeSyntax: TypeSyntax?) -> SyntacticSugarRuleViolation? {
|
|
||||||
if let optionalType = typeSyntax?.as(OptionalTypeSyntax.self) {
|
|
||||||
return violation(in: optionalType.wrappedType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let simpleType = typeSyntax?.as(SimpleTypeIdentifierSyntax.self) {
|
|
||||||
if SugaredType(typeName: simpleType.name.text) != nil {
|
|
||||||
return violation(from: simpleType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's no type, check all inner generics like in the case of 'Box<Array<T>>'
|
|
||||||
guard let genericArguments = simpleType.genericArgumentClause else { return nil }
|
|
||||||
let innerTypes = genericArguments.arguments.compactMap { violation(in: $0.argumentType) }
|
|
||||||
return innerTypes.first
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base class is "Swift" for cases like "Swift.Array"
|
|
||||||
if let memberType = typeSyntax?.as(MemberTypeIdentifierSyntax.self),
|
|
||||||
let baseType = memberType.baseType.as(SimpleTypeIdentifierSyntax.self),
|
|
||||||
baseType.name.text == "Swift" {
|
|
||||||
guard SugaredType(typeName: memberType.name.text) != nil else { return nil }
|
|
||||||
return violation(from: memberType)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func violation(from node: SyntaxProtocol & SyntaxWithGenericClause) -> SyntacticSugarRuleViolation? {
|
|
||||||
guard
|
|
||||||
let generic = node.genericArguments,
|
|
||||||
let firstGenericType = generic.arguments.first,
|
|
||||||
let lastGenericType = generic.arguments.last,
|
|
||||||
let typeName = node.typeName,
|
|
||||||
let type = SugaredType(typeName: typeName)
|
|
||||||
else { return nil }
|
|
||||||
|
|
||||||
let correctionType: SyntacticSugarRuleViolation.CorrectionType
|
|
||||||
switch type {
|
|
||||||
case .optional:
|
|
||||||
correctionType = .optional
|
|
||||||
case .array:
|
|
||||||
correctionType = .array
|
|
||||||
case .dictionary:
|
|
||||||
guard let comma = firstGenericType.trailingComma else { return nil }
|
|
||||||
let lastArgumentEnd = firstGenericType.argumentType.endPositionBeforeTrailingTrivia
|
|
||||||
correctionType = .dictionary(commaStart: lastArgumentEnd, commaEnd: comma.endPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
let firstInnerViolation = violation(in: firstGenericType.argumentType)
|
|
||||||
let secondInnerViolation = generic.arguments.count > 1 ? violation(in: lastGenericType.argumentType) : nil
|
|
||||||
|
|
||||||
return SyntacticSugarRuleViolation(
|
|
||||||
position: node.positionAfterSkippingLeadingTrivia,
|
|
||||||
type: type,
|
|
||||||
correction: .init(typeStart: node.position,
|
|
||||||
correction: correctionType,
|
|
||||||
leftStart: generic.leftAngleBracket.position,
|
|
||||||
leftEnd: generic.leftAngleBracket.endPosition,
|
|
||||||
rightStart: lastGenericType.endPositionBeforeTrailingTrivia,
|
|
||||||
rightEnd: generic.rightAngleBracket.endPositionBeforeTrailingTrivia),
|
|
||||||
children: [firstInnerViolation, secondInnerViolation].compactMap { $0 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct CorrectingContext {
|
|
||||||
let rule: Rule
|
|
||||||
let file: SwiftLintFile
|
|
||||||
var contents: String
|
|
||||||
var corrections: [Correction] = []
|
|
||||||
|
|
||||||
mutating func correctViolations(_ violations: [SyntacticSugarRuleViolation]) {
|
|
||||||
let sortedVolations = violations.sorted(by: { $0.correction.typeStart > $1.correction.typeStart })
|
|
||||||
for violation in sortedVolations {
|
|
||||||
correctViolation(violation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutating func correctViolation(_ violation: SyntacticSugarRuleViolation) {
|
|
||||||
let stringView = file.stringView
|
|
||||||
let correction = violation.correction
|
|
||||||
|
|
||||||
guard let violationNSRange = stringView.NSRange(start: correction.leftStart, end: correction.rightEnd),
|
|
||||||
file.ruleEnabled(violatingRange: violationNSRange, for: rule) != nil else { return }
|
|
||||||
|
|
||||||
guard let rightRange = stringView.NSRange(start: correction.rightStart, end: correction.rightEnd),
|
|
||||||
let leftRange = stringView.NSRange(start: correction.typeStart, end: correction.leftEnd) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch correction.correction {
|
|
||||||
case .array:
|
|
||||||
replaceCharacters(in: rightRange, with: "]")
|
|
||||||
correctViolations(violation.children)
|
|
||||||
replaceCharacters(in: leftRange, with: "[")
|
|
||||||
|
|
||||||
case let .dictionary(commaStart, commaEnd):
|
|
||||||
|
|
||||||
replaceCharacters(in: rightRange, with: "]")
|
|
||||||
guard let commaRange = stringView.NSRange(start: commaStart, end: commaEnd) else { return }
|
|
||||||
|
|
||||||
let violationsAfterComma = violation.children.filter { $0.position > commaStart }
|
|
||||||
correctViolations(violationsAfterComma)
|
|
||||||
|
|
||||||
replaceCharacters(in: commaRange, with: ": ")
|
|
||||||
|
|
||||||
let violationsBeforeComma = violation.children.filter { $0.position < commaStart }
|
|
||||||
correctViolations(violationsBeforeComma)
|
|
||||||
replaceCharacters(in: leftRange, with: "[")
|
|
||||||
|
|
||||||
case .optional:
|
|
||||||
replaceCharacters(in: rightRange, with: "?")
|
|
||||||
correctViolations(violation.children)
|
|
||||||
replaceCharacters(in: leftRange, with: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
let location = Location(file: file, byteOffset: ByteCount(correction.typeStart))
|
|
||||||
corrections.append(Correction(ruleDescription: type(of: rule).description, location: location))
|
|
||||||
}
|
|
||||||
|
|
||||||
private mutating func replaceCharacters(in range: NSRange, with replacement: String) {
|
|
||||||
contents = contents.bridge().replacingCharacters(in: range, with: replacement)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private protocol SyntaxWithGenericClause {
|
|
||||||
var typeName: String? { get }
|
|
||||||
var genericArguments: GenericArgumentClauseSyntax? { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension MemberTypeIdentifierSyntax: SyntaxWithGenericClause {
|
|
||||||
var typeName: String? { name.text }
|
|
||||||
var genericArguments: GenericArgumentClauseSyntax? { genericArgumentClause }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SimpleTypeIdentifierSyntax: SyntaxWithGenericClause {
|
|
||||||
var typeName: String? { name.text }
|
|
||||||
var genericArguments: GenericArgumentClauseSyntax? { genericArgumentClause }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SpecializeExprSyntax: SyntaxWithGenericClause {
|
|
||||||
var typeName: String? {
|
|
||||||
expression.as(IdentifierExprSyntax.self)?.firstToken(viewMode: .sourceAccurate)?.text ??
|
|
||||||
expression.as(MemberAccessExprSyntax.self)?.name.text
|
|
||||||
}
|
|
||||||
var genericArguments: GenericArgumentClauseSyntax? { genericArgumentClause }
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
internal enum SyntacticSugarRuleExamples {
|
|
||||||
static let nonTriggering = [
|
|
||||||
Example("let x: [Int]"),
|
|
||||||
Example("let x: [Int: String]"),
|
|
||||||
Example("let x: Int?"),
|
|
||||||
Example("func x(a: [Int], b: Int) -> [Int: Any]"),
|
|
||||||
Example("let x: Int!"),
|
|
||||||
Example("""
|
|
||||||
extension Array {
|
|
||||||
func x() { }
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
extension Dictionary {
|
|
||||||
func x() { }
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("let x: CustomArray<String>"),
|
|
||||||
Example("var currentIndex: Array<OnboardingPage>.Index?"),
|
|
||||||
Example("func x(a: [Int], b: Int) -> Array<Int>.Index"),
|
|
||||||
Example("unsafeBitCast(nonOptionalT, to: Optional<T>.self)"),
|
|
||||||
Example("unsafeBitCast(someType, to: Swift.Array<T>.self)"),
|
|
||||||
Example("IndexingIterator<Array<Dictionary<String, AnyObject>>>.self"),
|
|
||||||
Example("let y = Optional<String>.Type"),
|
|
||||||
|
|
||||||
Example("type is Optional<String>.Type"),
|
|
||||||
Example("let x: Foo.Optional<String>"),
|
|
||||||
|
|
||||||
Example("let x = case Optional<Any>.none = obj"),
|
|
||||||
Example("let a = Swift.Optional<String?>.none"),
|
|
||||||
Example("func f() -> [Array<Int>.Index] { [Array<Int>.Index]() }", excludeFromDocumentation: true)
|
|
||||||
]
|
|
||||||
|
|
||||||
static let triggering = [
|
|
||||||
Example("let x: ↓Array<String>"),
|
|
||||||
Example("let x: ↓Dictionary<Int, String>"),
|
|
||||||
Example("let x: ↓Optional<Int>"),
|
|
||||||
Example("let x: ↓Swift.Array<String>"),
|
|
||||||
|
|
||||||
Example("func x(a: ↓Array<Int>, b: Int) -> [Int: Any]"),
|
|
||||||
Example("func x(a: ↓Swift.Array<Int>, b: Int) -> [Int: Any]"),
|
|
||||||
|
|
||||||
Example("func x(a: [Int], b: Int) -> ↓Dictionary<Int, String>"),
|
|
||||||
Example("let x = y as? ↓Array<[String: Any]>"),
|
|
||||||
Example("let x = Box<Array<T>>()"),
|
|
||||||
Example("func x() -> Box<↓Array<T>>"),
|
|
||||||
Example("func x() -> ↓Dictionary<String, Any>?"),
|
|
||||||
|
|
||||||
Example("typealias Document = ↓Dictionary<String, T?>"),
|
|
||||||
Example("func x(_ y: inout ↓Array<T>)"),
|
|
||||||
Example("let x:↓Dictionary<String, ↓Dictionary<Int, Int>>"),
|
|
||||||
Example("func x() -> Any { return ↓Dictionary<Int, String>()}"),
|
|
||||||
|
|
||||||
Example("let x = ↓Array<String>.array(of: object)"),
|
|
||||||
Example("let x = ↓Swift.Array<String>.array(of: object)"),
|
|
||||||
|
|
||||||
Example("""
|
|
||||||
@_specialize(where S == ↓Array<Character>)
|
|
||||||
public init<S: Sequence>(_ elements: S)
|
|
||||||
"""),
|
|
||||||
|
|
||||||
Example("""
|
|
||||||
let dict: [String: Any] = [:]
|
|
||||||
_ = dict["key"] as? ↓Optional<String?> ?? Optional<String?>.none
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
static let corrections = [
|
|
||||||
Example("let x: Array<String>"): Example("let x: [String]"),
|
|
||||||
Example("let x: Array< String >"): Example("let x: [String]"),
|
|
||||||
Example("let x: Dictionary<Int, String>"): Example("let x: [Int: String]"),
|
|
||||||
Example("let x: Optional<Int>"): Example("let x: Int?"),
|
|
||||||
Example("let x: Optional< Int >"): Example("let x: Int?"),
|
|
||||||
|
|
||||||
Example("let x: Dictionary<Int , String>"): Example("let x: [Int: String]"),
|
|
||||||
Example("let x: Swift.Optional<String>"): Example("let x: String?"),
|
|
||||||
Example("let x:↓Dictionary<String, ↓Dictionary<Int, Int>>"): Example("let x:[String: [Int: Int]]"),
|
|
||||||
Example("let x:↓Dictionary<↓Dictionary<Int, Int>, String>"): Example("let x:[[Int: Int]: String]"),
|
|
||||||
Example("let x:↓Dictionary<↓Dictionary<↓Dictionary<Int, Int>, Int>, String>"):
|
|
||||||
Example("let x:[[[Int: Int]: Int]: String]"),
|
|
||||||
Example("let x:↓Array<↓Dictionary<Int, Int>>"): Example("let x:[[Int: Int]]"),
|
|
||||||
Example("let x:↓Optional<↓Dictionary<Int, Int>>"): Example("let x:[Int: Int]?")
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
import SwiftSyntaxBuilder
|
|
||||||
|
|
||||||
struct ToggleBoolRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static var description = RuleDescription(
|
|
||||||
identifier: "toggle_bool",
|
|
||||||
name: "Toggle Bool",
|
|
||||||
description: "Prefer `someBool.toggle()` over `someBool = !someBool`",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("isHidden.toggle()\n"),
|
|
||||||
Example("view.clipsToBounds.toggle()\n"),
|
|
||||||
Example("func foo() { abc.toggle() }"),
|
|
||||||
Example("view.clipsToBounds = !clipsToBounds\n"),
|
|
||||||
Example("disconnected = !connected\n"),
|
|
||||||
Example("result = !result.toggle()")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("↓isHidden = !isHidden\n"),
|
|
||||||
Example("↓view.clipsToBounds = !view.clipsToBounds\n"),
|
|
||||||
Example("func foo() { ↓abc = !abc }")
|
|
||||||
],
|
|
||||||
corrections: [
|
|
||||||
Example("↓isHidden = !isHidden\n"): Example("isHidden.toggle()\n"),
|
|
||||||
Example("↓view.clipsToBounds = !view.clipsToBounds\n"): Example("view.clipsToBounds.toggle()\n"),
|
|
||||||
Example("func foo() { ↓abc = !abc }"): Example("func foo() { abc.toggle() }")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
|
||||||
Rewriter(
|
|
||||||
locationConverter: file.locationConverter,
|
|
||||||
disabledRegions: disabledRegions(file: file)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ToggleBoolRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: ExprListSyntax) {
|
|
||||||
if node.hasToggleBoolViolation {
|
|
||||||
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: ExprListSyntax) -> ExprListSyntax {
|
|
||||||
guard
|
|
||||||
node.hasToggleBoolViolation,
|
|
||||||
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
|
||||||
else {
|
|
||||||
return super.visit(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
|
|
||||||
|
|
||||||
let newNode = node
|
|
||||||
.replacing(
|
|
||||||
childAt: 0,
|
|
||||||
with: "\(node.first!.trimmed).toggle()"
|
|
||||||
)
|
|
||||||
.removingLast()
|
|
||||||
.removingLast()
|
|
||||||
.with(\.leadingTrivia, node.leadingTrivia)
|
|
||||||
.with(\.trailingTrivia, node.trailingTrivia)
|
|
||||||
|
|
||||||
return super.visit(newNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ExprListSyntax {
|
|
||||||
var hasToggleBoolViolation: Bool {
|
|
||||||
guard
|
|
||||||
count == 3,
|
|
||||||
dropFirst().first?.is(AssignmentExprSyntax.self) == true,
|
|
||||||
last?.is(PrefixOperatorExprSyntax.self) == true,
|
|
||||||
let lhs = first?.trimmedDescription,
|
|
||||||
let rhs = last?.trimmedDescription,
|
|
||||||
rhs == "!\(lhs)"
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,127 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct UnavailableConditionRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "unavailable_condition",
|
|
||||||
name: "Unavailable Condition",
|
|
||||||
description: "Use #unavailable/#available instead of #available/#unavailable with an empty body.",
|
|
||||||
kind: .idiomatic,
|
|
||||||
minSwiftVersion: .fiveDotSix,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
if #unavailable(iOS 13) {
|
|
||||||
loadMainWindow()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if #available(iOS 9.0, *) {
|
|
||||||
doSomething()
|
|
||||||
} else {
|
|
||||||
legacyDoSomething()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if #available(macOS 11.0, *) {
|
|
||||||
// Do nothing
|
|
||||||
} else if #available(macOS 10.15, *) {
|
|
||||||
print("do some stuff")
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if #available(macOS 11.0, *) {
|
|
||||||
// Do nothing
|
|
||||||
} else if i > 7 {
|
|
||||||
print("do some stuff")
|
|
||||||
} else if i < 2, #available(macOS 11.0, *) {
|
|
||||||
print("something else")
|
|
||||||
}
|
|
||||||
""", excludeFromDocumentation: true)
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
if ↓#available(iOS 14.0) {
|
|
||||||
|
|
||||||
} else {
|
|
||||||
oldIos13TrackingLogic(isEnabled: ASIdentifierManager.shared().isAdvertisingTrackingEnabled)
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if ↓#available(iOS 14.0) {
|
|
||||||
// we don't need to do anything here
|
|
||||||
} else {
|
|
||||||
oldIos13TrackingLogic(isEnabled: ASIdentifierManager.shared().isAdvertisingTrackingEnabled)
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if ↓#available(iOS 13, *) {} else {
|
|
||||||
loadMainWindow()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
if ↓#unavailable(iOS 13) {
|
|
||||||
// Do nothing
|
|
||||||
} else if i < 2 {
|
|
||||||
loadMainWindow()
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
UnavailableConditionRuleVisitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class UnavailableConditionRuleVisitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: IfExprSyntax) {
|
|
||||||
guard node.body.statements.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let condition = node.conditions.onlyElement,
|
|
||||||
let availability = asAvailabilityCondition(condition.condition) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if otherAvailabilityCheckInvolved(ifStmt: node) {
|
|
||||||
// If there are other conditional branches with availability checks it might not be possible
|
|
||||||
// to just invert the first one.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(
|
|
||||||
ReasonedRuleViolation(
|
|
||||||
position: availability.positionAfterSkippingLeadingTrivia,
|
|
||||||
reason: reason(for: availability)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func asAvailabilityCondition(_ condition: ConditionElementSyntax.Condition)
|
|
||||||
-> AvailabilityConditionSyntax? {
|
|
||||||
condition.as(AvailabilityConditionSyntax.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func otherAvailabilityCheckInvolved(ifStmt: IfExprSyntax) -> Bool {
|
|
||||||
if let elseBody = ifStmt.elseBody, let nestedIfStatement = elseBody.as(IfExprSyntax.self) {
|
|
||||||
if nestedIfStatement.conditions.map(\.condition).compactMap(asAvailabilityCondition).isNotEmpty {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return otherAvailabilityCheckInvolved(ifStmt: nestedIfStatement)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reason(for condition: AvailabilityConditionSyntax) -> String {
|
|
||||||
switch condition.availabilityKeyword.tokenKind {
|
|
||||||
case .poundAvailableKeyword:
|
|
||||||
return "Use #unavailable instead of #available with an empty body"
|
|
||||||
case .poundUnavailableKeyword:
|
|
||||||
return "Use #available instead of #unavailable with an empty body"
|
|
||||||
default:
|
|
||||||
queuedFatalError("Unknown availability check type.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,67 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
private func embedInSwitch(
|
|
||||||
_ text: String,
|
|
||||||
case: String = "case .bar",
|
|
||||||
file: StaticString = #file, line: UInt = #line) -> Example {
|
|
||||||
return Example("""
|
|
||||||
switch foo {
|
|
||||||
\(`case`):
|
|
||||||
\(text)
|
|
||||||
}
|
|
||||||
""", file: file, line: line)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UnneededBreakInSwitchRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "unneeded_break_in_switch",
|
|
||||||
name: "Unneeded Break in Switch",
|
|
||||||
description: "Avoid using unneeded break statements",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
embedInSwitch("break"),
|
|
||||||
embedInSwitch("break", case: "default"),
|
|
||||||
embedInSwitch("for i in [0, 1, 2] { break }"),
|
|
||||||
embedInSwitch("if true { break }"),
|
|
||||||
embedInSwitch("something()"),
|
|
||||||
Example("""
|
|
||||||
let items = [Int]()
|
|
||||||
for item in items {
|
|
||||||
if bar() {
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} catch {
|
|
||||||
bar()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
embedInSwitch("something()\n ↓break"),
|
|
||||||
embedInSwitch("something()\n ↓break // comment"),
|
|
||||||
embedInSwitch("something()\n ↓break", case: "default"),
|
|
||||||
embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
UnneededBreakInSwitchRuleVisitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class UnneededBreakInSwitchRuleVisitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: SwitchCaseSyntax) {
|
|
||||||
guard node.statements.count > 1,
|
|
||||||
let statement = node.statements.last,
|
|
||||||
let breakStatement = statement.item.as(BreakStmtSyntax.self),
|
|
||||||
breakStatement.label == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(statement.item.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct UntypedErrorInCatchRule: OptInRule, ConfigurationProviderRule, SwiftSyntaxCorrectableRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "untyped_error_in_catch",
|
|
||||||
name: "Untyped Error in Catch",
|
|
||||||
description: "Catch statements should not declare error variables without type casting",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} catch {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} catch Error.invalidOperation {
|
|
||||||
} catch {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} catch let error as MyError {
|
|
||||||
} catch {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} catch var error as MyError {
|
|
||||||
} catch {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try something()
|
|
||||||
} catch let e where e.code == .fileError {
|
|
||||||
// can be ignored
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch var error {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch let error {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch let someError {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch var someError {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch let e {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch(let error) {}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
do {
|
|
||||||
try foo()
|
|
||||||
} ↓catch (let error) {}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
corrections: [
|
|
||||||
Example("do {\n try foo() \n} ↓catch let error {}"): Example("do {\n try foo() \n} catch {}"),
|
|
||||||
Example("do {\n try foo() \n} ↓catch(let error) {}"): Example("do {\n try foo() \n} catch {}"),
|
|
||||||
Example("do {\n try foo() \n} ↓catch (let error) {}"): Example("do {\n try foo() \n} catch {}")
|
|
||||||
])
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
UntypedErrorInCatchRuleVisitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeRewriter(file: SwiftLintFile) -> ViolationsSyntaxRewriter? {
|
|
||||||
UntypedErrorInCatchRuleRewriter(
|
|
||||||
locationConverter: file.locationConverter,
|
|
||||||
disabledRegions: disabledRegions(file: file)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension CatchItemSyntax {
|
|
||||||
var isIdentifierPattern: Bool {
|
|
||||||
guard whereClause == nil else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if let pattern = pattern?.as(ValueBindingPatternSyntax.self) {
|
|
||||||
return pattern.valuePattern.is(IdentifierPatternSyntax.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let pattern = pattern?.as(ExpressionPatternSyntax.self),
|
|
||||||
let tupleExpr = pattern.expression.as(TupleExprSyntax.self),
|
|
||||||
let tupleElement = tupleExpr.elementList.onlyElement,
|
|
||||||
let unresolvedPattern = tupleElement.expression.as(UnresolvedPatternExprSyntax.self),
|
|
||||||
let valueBindingPattern = unresolvedPattern.pattern.as(ValueBindingPatternSyntax.self) {
|
|
||||||
return valueBindingPattern.valuePattern.is(IdentifierPatternSyntax.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class UntypedErrorInCatchRuleVisitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: CatchClauseSyntax) {
|
|
||||||
guard let item = node.catchItems?.onlyElement,
|
|
||||||
item.isIdentifierPattern else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(node.catchKeyword.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class UntypedErrorInCatchRuleRewriter: 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: CatchClauseSyntax) -> CatchClauseSyntax {
|
|
||||||
guard
|
|
||||||
let item = node.catchItems?.onlyElement,
|
|
||||||
item.isIdentifierPattern,
|
|
||||||
!node.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
|
|
||||||
else {
|
|
||||||
return super.visit(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
correctionPositions.append(node.catchKeyword.positionAfterSkippingLeadingTrivia)
|
|
||||||
return super.visit(
|
|
||||||
node
|
|
||||||
.with(\.catchKeyword, node.catchKeyword.with(\.trailingTrivia, .spaces(1)))
|
|
||||||
.with(\.catchItems, CatchItemListSyntax([]))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,227 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct VoidFunctionInTernaryConditionRule: ConfigurationProviderRule, SwiftSyntaxRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "void_function_in_ternary",
|
|
||||||
name: "Void Function in Ternary",
|
|
||||||
description: "Using ternary to call Void functions should be avoided",
|
|
||||||
kind: .idiomatic,
|
|
||||||
minSwiftVersion: .fiveDotOne,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("let result = success ? foo() : bar()"),
|
|
||||||
Example("""
|
|
||||||
if success {
|
|
||||||
askQuestion()
|
|
||||||
} else {
|
|
||||||
exit()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
var price: Double {
|
|
||||||
return hasDiscount ? calculatePriceWithDiscount() : calculateRegularPrice()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("foo(x == 2 ? a() : b())"),
|
|
||||||
Example("""
|
|
||||||
chevronView.image = collapsed ? .icon(.mediumChevronDown) : .icon(.mediumChevronUp)
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
array.map { elem in
|
|
||||||
elem.isEmpty() ? .emptyValue() : .number(elem)
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func compute(data: [Int]) -> Int {
|
|
||||||
data.isEmpty ? 0 : expensiveComputation(data)
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
var value: Int {
|
|
||||||
mode == .fast ? fastComputation() : expensiveComputation()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
var value: Int {
|
|
||||||
get {
|
|
||||||
mode == .fast ? fastComputation() : expensiveComputation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
subscript(index: Int) -> Int {
|
|
||||||
get {
|
|
||||||
index == 0 ? defaultValue() : compute(index)
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
subscript(index: Int) -> Int {
|
|
||||||
index == 0 ? defaultValue() : compute(index)
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("success ↓? askQuestion() : exit()"),
|
|
||||||
Example("""
|
|
||||||
perform { elem in
|
|
||||||
elem.isEmpty() ↓? .emptyValue() : .number(elem)
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.sectionViewModels[section].collapsed.toggle()
|
|
||||||
self.sectionViewModels[section].collapsed
|
|
||||||
↓? self.tableView.deleteRows(at: [IndexPath(row: 0, section: section)], with: .automatic)
|
|
||||||
: self.tableView.insertRows(at: [IndexPath(row: 0, section: section)], with: .automatic)
|
|
||||||
self.tableView.scrollToRow(at: IndexPath(row: NSNotFound, section: section), at: .top, animated: true)
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
subscript(index: Int) -> Int {
|
|
||||||
index == 0 ↓? something() : somethingElse(index)
|
|
||||||
return index
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
var value: Int {
|
|
||||||
mode == .fast ↓? something() : somethingElse()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
var value: Int {
|
|
||||||
get {
|
|
||||||
mode == .fast ↓? something() : somethingElse()
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
subscript(index: Int) -> Int {
|
|
||||||
get {
|
|
||||||
index == 0 ↓? something() : somethingElse(index)
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
VoidFunctionInTernaryConditionVisitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class VoidFunctionInTernaryConditionVisitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: TernaryExprSyntax) {
|
|
||||||
guard node.firstChoice.is(FunctionCallExprSyntax.self),
|
|
||||||
node.secondChoice.is(FunctionCallExprSyntax.self),
|
|
||||||
let parent = node.parent?.as(ExprListSyntax.self),
|
|
||||||
!parent.containsAssignment,
|
|
||||||
let grandparent = parent.parent,
|
|
||||||
grandparent.is(SequenceExprSyntax.self),
|
|
||||||
let blockItem = grandparent.parent?.as(CodeBlockItemSyntax.self),
|
|
||||||
!blockItem.isImplicitReturn else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(node.questionMark.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func visitPost(_ node: UnresolvedTernaryExprSyntax) {
|
|
||||||
guard node.firstChoice.is(FunctionCallExprSyntax.self),
|
|
||||||
let parent = node.parent?.as(ExprListSyntax.self),
|
|
||||||
parent.last?.is(FunctionCallExprSyntax.self) == true,
|
|
||||||
!parent.containsAssignment,
|
|
||||||
let grandparent = parent.parent,
|
|
||||||
grandparent.is(SequenceExprSyntax.self),
|
|
||||||
let blockItem = grandparent.parent?.as(CodeBlockItemSyntax.self),
|
|
||||||
!blockItem.isImplicitReturn else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(node.questionMark.positionAfterSkippingLeadingTrivia)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ExprListSyntax {
|
|
||||||
var containsAssignment: Bool {
|
|
||||||
return children(viewMode: .sourceAccurate).contains(where: { $0.is(AssignmentExprSyntax.self) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension CodeBlockItemSyntax {
|
|
||||||
var isImplicitReturn: Bool {
|
|
||||||
isClosureImplictReturn || isFunctionImplicitReturn ||
|
|
||||||
isVariableImplicitReturn || isSubscriptImplicitReturn ||
|
|
||||||
isAcessorImplicitReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
var isClosureImplictReturn: Bool {
|
|
||||||
guard let parent = parent?.as(CodeBlockItemListSyntax.self),
|
|
||||||
let grandparent = parent.parent else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent.children(viewMode: .sourceAccurate).count == 1 && grandparent.is(ClosureExprSyntax.self)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isFunctionImplicitReturn: Bool {
|
|
||||||
guard let parent = parent?.as(CodeBlockItemListSyntax.self),
|
|
||||||
let functionDecl = parent.parent?.parent?.as(FunctionDeclSyntax.self) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent.children(viewMode: .sourceAccurate).count == 1 && functionDecl.signature.allowsImplicitReturns
|
|
||||||
}
|
|
||||||
|
|
||||||
var isVariableImplicitReturn: Bool {
|
|
||||||
guard let parent = parent?.as(CodeBlockItemListSyntax.self) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let isVariableDecl = parent.parent?.parent?.as(PatternBindingSyntax.self) != nil
|
|
||||||
return parent.children(viewMode: .sourceAccurate).count == 1 && isVariableDecl
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSubscriptImplicitReturn: Bool {
|
|
||||||
guard let parent = parent?.as(CodeBlockItemListSyntax.self),
|
|
||||||
let subscriptDecl = parent.parent?.parent?.as(SubscriptDeclSyntax.self) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent.children(viewMode: .sourceAccurate).count == 1 && subscriptDecl.allowsImplicitReturns
|
|
||||||
}
|
|
||||||
|
|
||||||
var isAcessorImplicitReturn: Bool {
|
|
||||||
guard let parent = parent?.as(CodeBlockItemListSyntax.self),
|
|
||||||
parent.parent?.parent?.as(AccessorDeclSyntax.self) != nil else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent.children(viewMode: .sourceAccurate).count == 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension FunctionSignatureSyntax {
|
|
||||||
var allowsImplicitReturns: Bool {
|
|
||||||
output?.allowsImplicitReturns ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension SubscriptDeclSyntax {
|
|
||||||
var allowsImplicitReturns: Bool {
|
|
||||||
result.allowsImplicitReturns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension ReturnClauseSyntax {
|
|
||||||
var allowsImplicitReturns: Bool {
|
|
||||||
if let simpleType = returnType.as(SimpleTypeIdentifierSyntax.self) {
|
|
||||||
return simpleType.name.text != "Void" && simpleType.name.text != "Never"
|
|
||||||
} else if let tupleType = returnType.as(TupleTypeSyntax.self) {
|
|
||||||
return !tupleType.elements.isEmpty
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import SwiftSyntax
|
|
||||||
|
|
||||||
struct XCTFailMessageRule: SwiftSyntaxRule, ConfigurationProviderRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "xctfail_message",
|
|
||||||
name: "XCTFail Message",
|
|
||||||
description: "An XCTFail call should include a description of the assertion",
|
|
||||||
kind: .idiomatic,
|
|
||||||
nonTriggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
func testFoo() {
|
|
||||||
XCTFail("bar")
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func testFoo() {
|
|
||||||
XCTFail(bar)
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
],
|
|
||||||
triggeringExamples: [
|
|
||||||
Example("""
|
|
||||||
func testFoo() {
|
|
||||||
↓XCTFail()
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
func testFoo() {
|
|
||||||
↓XCTFail("")
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
|
||||||
Visitor(viewMode: .sourceAccurate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension XCTFailMessageRule {
|
|
||||||
final class Visitor: ViolationsSyntaxVisitor {
|
|
||||||
override func visitPost(_ node: FunctionCallExprSyntax) {
|
|
||||||
guard
|
|
||||||
let expression = node.calledExpression.as(IdentifierExprSyntax.self),
|
|
||||||
expression.identifier.text == "XCTFail",
|
|
||||||
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,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,141 +0,0 @@
|
||||||
import SourceKittenFramework
|
|
||||||
|
|
||||||
/// In UIKit, a `UIImageView` was by default not an accessibility element, and would only be visible to VoiceOver
|
|
||||||
/// and other assistive technologies if the developer explicitly made them an accessibility element. In SwiftUI,
|
|
||||||
/// however, an `Image` is an accessibility element by default. If the developer does not explicitly hide them from
|
|
||||||
/// accessibility or give them an accessibility label, they will inherit the name of the image file, which often creates
|
|
||||||
/// a poor experience when VoiceOver reads things like "close icon white".
|
|
||||||
///
|
|
||||||
/// Known false negatives for Images declared as instance variables and containers that provide a label but are
|
|
||||||
/// not accessibility elements. Known false positives for Images created in a separate function from where they
|
|
||||||
/// have accessibility properties applied.
|
|
||||||
struct AccessibilityLabelForImageRule: ASTRule, ConfigurationProviderRule, OptInRule {
|
|
||||||
var configuration = SeverityConfiguration<Self>(.warning)
|
|
||||||
|
|
||||||
static let description = RuleDescription(
|
|
||||||
identifier: "accessibility_label_for_image",
|
|
||||||
name: "Accessibility Label for Image",
|
|
||||||
description: "Images that provide context should have an accessibility label or should be explicitly hidden " +
|
|
||||||
"from accessibility",
|
|
||||||
kind: .lint,
|
|
||||||
minSwiftVersion: .fiveDotOne,
|
|
||||||
nonTriggeringExamples: AccessibilityLabelForImageRuleExamples.nonTriggeringExamples,
|
|
||||||
triggeringExamples: AccessibilityLabelForImageRuleExamples.triggeringExamples
|
|
||||||
)
|
|
||||||
|
|
||||||
// MARK: AST Rule
|
|
||||||
|
|
||||||
func validate(file: SwiftLintFile, kind: SwiftDeclarationKind,
|
|
||||||
dictionary: SourceKittenDictionary) -> [StyleViolation] {
|
|
||||||
// Only proceed to check View structs.
|
|
||||||
guard kind == .struct,
|
|
||||||
dictionary.inheritedTypes.contains("View"),
|
|
||||||
dictionary.substructure.isNotEmpty else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return findImageViolations(file: file, substructure: dictionary.substructure)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively check a file for image violations, and return all such violations.
|
|
||||||
private func findImageViolations(file: SwiftLintFile, substructure: [SourceKittenDictionary]) -> [StyleViolation] {
|
|
||||||
var violations = [StyleViolation]()
|
|
||||||
for dictionary in substructure {
|
|
||||||
guard let offset: ByteCount = dictionary.offset else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's image, and does not hide from accessibility or provide a label, it's a violation.
|
|
||||||
if dictionary.isImage {
|
|
||||||
if dictionary.isDecorativeOrLabeledOrSystemImage ||
|
|
||||||
dictionary.hasAccessibilityHiddenModifier(in: file) ||
|
|
||||||
dictionary.hasAccessibilityLabelModifier(in: file) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(
|
|
||||||
StyleViolation(ruleDescription: Self.description,
|
|
||||||
severity: configuration.severity,
|
|
||||||
location: Location(file: file, byteOffset: offset))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If dictionary did not represent an Image, recursively check substructure,
|
|
||||||
// unless it's a container that hides its children from accessibility or is labeled.
|
|
||||||
else if dictionary.substructure.isNotEmpty {
|
|
||||||
if dictionary.hasAccessibilityHiddenModifier(in: file) ||
|
|
||||||
dictionary.hasAccessibilityElementChildrenIgnoreModifier(in: file) ||
|
|
||||||
dictionary.hasAccessibilityLabelModifier(in: file) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
violations.append(contentsOf: findImageViolations(file: file, substructure: dictionary.substructure))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return violations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: SourceKittenDictionary extensions
|
|
||||||
|
|
||||||
private extension SourceKittenDictionary {
|
|
||||||
/// Whether or not the dictionary represents a SwiftUI Image.
|
|
||||||
/// Currently only accounts for SwiftUI image literals and not instance variables.
|
|
||||||
var isImage: Bool {
|
|
||||||
// Image literals will be reported as calls to the initializer.
|
|
||||||
guard expressionKind == .call else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if name == "Image" || name == "SwiftUI.Image" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively check substructure.
|
|
||||||
// SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like:
|
|
||||||
// Image(decorative: "myImage").resizable().frame
|
|
||||||
// --> Image(decorative: "myImage").resizable
|
|
||||||
// --> Image
|
|
||||||
return substructure.contains(where: { $0.isImage })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the dictionary represents a SwiftUI Image using the `Image(decorative:)` constructor (hides
|
|
||||||
/// from a11y), or one of the `Image(_:label:)` or `Image(systemName:)` constructors (provides label).
|
|
||||||
var isDecorativeOrLabeledOrSystemImage: Bool {
|
|
||||||
guard isImage else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for Image(decorative:), Image(_:label:), or Image(systemName:) constructor.
|
|
||||||
if expressionKind == .call &&
|
|
||||||
enclosedArguments.contains(where: { ["decorative", "label", "systemName"].contains($0.name) }) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively check substructure.
|
|
||||||
// SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like:
|
|
||||||
// Image(decorative: "myImage").resizable().frame
|
|
||||||
// --> Image(decorative: "myImage").resizable
|
|
||||||
// --> Image
|
|
||||||
return substructure.contains(where: { $0.isDecorativeOrLabeledOrSystemImage })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the dictionary represents a SwiftUI View with an `accesibilityLabel(_:)`
|
|
||||||
/// or `accessibility(label:)` modifier.
|
|
||||||
func hasAccessibilityLabelModifier(in file: SwiftLintFile) -> Bool {
|
|
||||||
return hasModifier(
|
|
||||||
anyOf: [
|
|
||||||
SwiftUIModifier(
|
|
||||||
name: "accessibilityLabel",
|
|
||||||
arguments: [.init(name: "", values: [])]
|
|
||||||
),
|
|
||||||
SwiftUIModifier(
|
|
||||||
name: "accessibility",
|
|
||||||
arguments: [.init(name: "label", values: [])]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
in: file
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,269 +0,0 @@
|
||||||
internal struct AccessibilityLabelForImageRuleExamples {
|
|
||||||
static let nonTriggeringExamples = [
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image(decorative: "my-image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image(systemName: "circle.plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image("my-image", label: Text("Alt text for my image"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(hidden: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image("my-image")
|
|
||||||
.accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(label: Text("Alt text for my image"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image("my-image")
|
|
||||||
.accessibilityLabel(Text("Alt text for my image"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image(uiImage: myUiImage)
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image(uiImage: myUiImage)
|
|
||||||
.accessibilityLabel(Text("Alt text for my image"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
SwiftUI.Image(uiImage: "my-image").resizable().accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image(decorative: "my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(label: Text("Alt text for my image"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(label: Text("Alt text for my image"))
|
|
||||||
}.accessibilityElement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(label: Text("Alt text for my image"))
|
|
||||||
}.accessibilityHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image(decorative: "my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Text("Text to accompany my image")
|
|
||||||
}.accessibilityElement(children: .combine)
|
|
||||||
.padding(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Text("Text to accompany my image")
|
|
||||||
}.accessibilityElement(children: .ignore)
|
|
||||||
.padding(16)
|
|
||||||
.accessibilityLabel(Text("Label for my image and text"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Button(action: { doAction() }) {
|
|
||||||
Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
.accessibilityLabel(Text("Label for my image"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
|
|
||||||
static let triggeringExamples = [
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
↓Image("my-image")
|
|
||||||
.resizable(true)
|
|
||||||
.frame(width: 48, height: 48)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
↓Image(uiImage: myUiImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
↓SwiftUI.Image(uiImage: "my-image").resizable().accessibilityHidden(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image(uiImage: myUiImage)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 48, height: 48)
|
|
||||||
.accessibilityLabel(Text("Alt text for my image"))
|
|
||||||
↓Image("other image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Image(decorative: "image1")
|
|
||||||
↓Image("image2")
|
|
||||||
Image(uiImage: "image3")
|
|
||||||
.accessibility(label: Text("a pretty picture"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Image(decorative: "my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
↓Image("my-image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
↓Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(label: Text("Alt text for my image"))
|
|
||||||
}.accessibilityElement(children: .contain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
↓Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Image("my-image")
|
|
||||||
.accessibility(label: Text("Alt text for my image"))
|
|
||||||
}.accessibilityHidden(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
↓Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
Text("Text to accompany my image")
|
|
||||||
}.accessibilityElement(children: .combine)
|
|
||||||
.padding(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""),
|
|
||||||
Example("""
|
|
||||||
struct MyView: View {
|
|
||||||
var body: some View {
|
|
||||||
Button(action: { doAction() }) {
|
|
||||||
↓Image("my-image")
|
|
||||||
.renderingMode(.template)
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
]
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue