Compare commits

..

7 Commits

Author SHA1 Message Date
JP Simard ddcde3faf1
fixup! Fall back to file system traversal if config files changed 2021-03-08 20:49:26 -05:00
JP Simard adc92db7b6
Fall back to file system traversal if config files changed 2021-03-08 18:02:54 -05:00
JP Simard b5421db0f5
Move specifying git revision from config to CLI 2021-03-08 18:02:53 -05:00
JP Simard 204c32481e
Fix typos 2021-03-08 18:00:47 -05:00
JP Simard 6b59bf0d3e
Docs & dead code 2021-03-08 18:00:47 -05:00
JP Simard e996f50c05
Lint all files changed compared to the merge-base
Between HEAD and the stable git revision. Also include untracked files.
2021-03-08 18:00:47 -05:00
JP Simard 6483fc142a
Add "stable git revision" to configuration
This allows specifying a git revision or "commit-ish" that is considered
stable. If specified, SwifLint will attempt to query the git repository
index for files changed since that revision, using only those files as
input as opposed to traversing the file system to collect lintable
files.
2021-03-08 18:00:47 -05:00
835 changed files with 25001 additions and 38746 deletions

View File

@ -1 +0,0 @@
.build

View File

@ -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

View File

@ -1 +0,0 @@
6.2.0

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -1,46 +0,0 @@
name: docker
on:
push:
branches:
- main
tags:
- '*'
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Extract DOCKER_TAG using tag name
if: startsWith(github.ref, 'refs/tags/')
run: |
echo "DOCKER_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Use default DOCKER_TAG
if: startsWith(github.ref, 'refs/tags/') != true
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Set lowercase repository name
run: |
echo "REPOSITORY_LC=${REPOSITORY,,}" >>${GITHUB_ENV}
env:
REPOSITORY: '${{ github.repository }}'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to Github registry
uses: docker/login-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: ghcr.io
- uses: docker/build-push-action@v2
with:
push: true
tags: ghcr.io/${{ env.REPOSITORY_LC }}:${{ env.DOCKER_TAG }}

14
.gitignore vendored
View File

@ -1,5 +1,6 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
@ -21,7 +22,6 @@ xcuserdata
*.moved-aside
*.xcuserstate
*.xcscmblueprint
default.profraw
## Obj-C/Swift specific
*.hmap
@ -37,17 +37,14 @@ default.profraw
# SwiftLint
SwiftLint.xcodeproj
SwiftLint.pkg
*.zip
SwiftLintFramework.framework.zip
benchmark_*
portable_swiftlint.zip
swiftlint_linux.zip
osscheck/
docs/
rule_docs/
bazel.tar.gz
bazel.tar.gz.sha256
ci.bazelrc
user.bazelrc
# Swift Package Manager
#
@ -62,6 +59,3 @@ Packages/
# Bundler
.bundle/
bundle/
# Bazel
bazel-*

View File

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

View File

@ -1,6 +0,0 @@
- id: swiftlint
name: SwiftLint
description: "Check Swift files for issues with SwiftLint"
entry: "swiftlint --quiet"
language: swift
types: [swift]

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}])

View File

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

View File

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

View File

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

211
BUILD
View File

@ -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",
],
)

File diff suppressed because it is too large Load Diff

View File

@ -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
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
merging `main` into your PR branch to update it and resolve conflicts.
should never be made directly on the `master` branch. Prefer rebasing over
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
and not worth waiting for review, you may open a pull request and merge
@ -31,7 +25,7 @@ to execute SwiftLint. A folder that contains swift source files.
|Arguments|Options|
|-|-|
|![image](https://user-images.githubusercontent.com/5748627/115156411-d38c8780-a08c-11eb-9de4-939606c81574.png)|![image](https://user-images.githubusercontent.com/5748627/115156276-287bce00-a08c-11eb-9e1d-35684a665228.png)|
|![image](https://user-images.githubusercontent.com/474794/93893073-1676b000-fca2-11ea-9428-20423c5f5237.png)|![image](https://user-images.githubusercontent.com/474794/93893135-27bfbc80-fca2-11ea-805a-ee21b446c3f0.png)|
Then you can use the full power of Xcode/LLDB/Instruments to develop and debug your changes to SwiftLint.
@ -64,7 +58,7 @@ $ make docker_test
## Rules
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory.
New rules should be added in the `Source/SwiftLintFramework/Rules` directory.
Rules should conform to either the `Rule` or `ASTRule` protocols.
@ -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
`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`
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
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,
[`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,
[`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:
``` 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.
When issuing a pull request with user-facing changes, please add a
summary of your changes to the `CHANGELOG.md` file.
When issuing a pull request, please add a summary of your changes to
the `CHANGELOG.md` file.
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,
you may instead link to the change's pull request.
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

View File

@ -14,13 +14,11 @@ modified_files = git.modified_files + git.added_files
# including in a CHANGELOG for example
has_app_changes = !modified_files.grep(/Source/).empty?
has_test_changes = !modified_files.grep(/Tests/).empty?
has_danger_changes = !modified_files.grep(/Dangerfile|tools\/oss-check|Gemfile/).empty?
has_package_changes = !modified_files.grep(/Package\.swift/).empty?
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD|MODULE\.bazel/).empty?
has_danger_changes = !modified_files.grep(/Dangerfile|script\/oss-check|Gemfile/).empty?
# Add a CHANGELOG entry for 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
Here's an example of your CHANGELOG entry:
```markdown
@ -32,7 +30,7 @@ Here's an example of your CHANGELOG entry:
MARKDOWN
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
if git.lines_of_code > 50 && has_app_changes && !has_test_changes
@ -51,8 +49,7 @@ end
file = Tempfile.new('violations')
force_flag = has_danger_changes ? "--force" : ""
Open3.popen3("tools/oss-check -v #{force_flag} 2> #{file.path}") do |_, stdout, _, _|
Open3.popen3("script/oss-check -v 2> #{file.path}") do |_, stdout, _, _|
while char = stdout.getc
print char
end

View File

@ -1,6 +1,6 @@
# Explicitly specify `jammy` to keep the Swift & Ubuntu images in sync.
ARG BUILDER_IMAGE=swift:jammy
ARG RUNTIME_IMAGE=ubuntu:jammy
# Explicitly specify `bionic` because `swift:latest` does not use `ubuntu:latest`.
ARG BUILDER_IMAGE=swift:bionic
ARG RUNTIME_IMAGE=ubuntu:bionic
# builder image
FROM ${BUILDER_IMAGE} AS builder
@ -9,16 +9,16 @@ RUN apt-get update && apt-get install -y \
libxml2-dev \
&& rm -r /var/lib/apt/lists/*
WORKDIR /workdir/
COPY Plugins Plugins/
COPY Source Source/
COPY Tests Tests/
COPY Package.* ./
RUN swift package update
ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib -Xlinker -lCFURLSessionInterface -Xlinker -lCFXMLInterface -Xlinker -lcurl -Xlinker -lxml2 -Xswiftc -I. -Xlinker -fuse-ld=lld -Xlinker -L/usr/lib/swift/linux"
RUN swift build $SWIFT_FLAGS --product swiftlint
ARG SWIFT_FLAGS="-c release -Xswiftc -static-stdlib"
RUN swift build $SWIFT_FLAGS
RUN mkdir -p /executables
RUN install -v `swift build $SWIFT_FLAGS --show-bin-path`/swiftlint /executables
RUN for executable in $(swift package completion-tool list-executables); do \
install -v `swift build $SWIFT_FLAGS --show-bin-path`/$executable /executables; \
done
# runtime image
FROM ${RUNTIME_IMAGE}
@ -30,10 +30,8 @@ RUN apt-get update && apt-get install -y \
COPY --from=builder /usr/lib/libsourcekitdInProc.so /usr/lib
COPY --from=builder /usr/lib/swift/linux/libBlocksRuntime.so /usr/lib
COPY --from=builder /usr/lib/swift/linux/libdispatch.so /usr/lib
COPY --from=builder /usr/lib/swift/linux/libswiftCore.so /usr/lib
COPY --from=builder /executables/* /usr/bin
RUN swiftlint version
RUN echo "_ = 0" | swiftlint --use-stdin
CMD ["swiftlint"]

View File

@ -1,157 +1,151 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.6)
rexml
activesupport (7.0.4.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
algoliasearch (1.27.5)
CFPropertyList (3.0.2)
activesupport (4.2.11.1)
i18n (~> 0.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.1)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.1.0)
claide (1.0.3)
claide-plugins (0.9.2)
cork
nap
open4 (~> 1.3)
cocoapods (1.12.1)
addressable (~> 2.8)
cocoapods (1.9.1)
activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.12.1)
cocoapods-core (= 1.9.1)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.6.0, < 2.0)
cocoapods-downloader (>= 1.2.2, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
molinillo (~> 0.6.6)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.21.0, < 2.0)
cocoapods-core (1.12.1)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
ruby-macho (~> 1.4)
xcodeproj (>= 1.14.0, < 2.0)
cocoapods-core (1.9.1)
activesupport (>= 4.0.2, < 6)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
cocoapods-try (1.1.0)
colored2 (3.1.2)
concurrent-ruby (1.2.2)
concurrent-ruby (1.1.6)
cork (0.3.0)
colored2 (~> 3.1)
danger (9.2.0)
danger (6.3.2)
claide (~> 1.0)
claide-plugins (>= 0.9.2)
colored2 (~> 3.1)
cork (~> 0.1)
faraday (>= 0.9.0, < 3.0)
faraday (~> 0.9)
faraday-http-cache (~> 2.0)
git (~> 1.7)
kramdown (~> 2.3)
git (~> 1.6)
kramdown (~> 2.0)
kramdown-parser-gfm (~> 1.0)
no_proxy_fix
octokit (~> 5.0)
terminal-table (>= 1, < 4)
octokit (~> 4.7)
terminal-table (~> 1)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
faraday (2.7.4)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-http-cache (2.4.1)
faraday (>= 0.8)
faraday-net_http (3.0.2)
ffi (1.15.5)
ethon (0.12.0)
ffi (>= 1.3.0)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday-http-cache (2.0.0)
faraday (~> 0.8)
ffi (1.12.2)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
git (1.18.0)
addressable (~> 2.8)
git (1.6.0)
rchardet (~> 1.8)
httpclient (2.8.3)
i18n (1.12.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jazzy (0.14.3)
jazzy (0.13.2)
cocoapods (~> 1.5)
mustache (~> 1.1)
open4 (~> 1.3)
open4
redcarpet (~> 3.4)
rexml (~> 3.2)
rouge (>= 2.0.6, < 4.0)
sassc (~> 2.1)
sqlite3 (~> 1.3)
xcinvoke (~> 0.3.0)
json (2.6.3)
kramdown (2.4.0)
json (2.3.0)
kramdown (2.3.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liferaft (0.0.6)
minitest (5.18.0)
molinillo (0.8.0)
minitest (5.14.0)
molinillo (0.6.6)
multipart-post (2.1.1)
mustache (1.1.1)
nanaimo (0.3.0)
nanaimo (0.2.6)
nap (1.1.0)
netrc (0.11.0)
no_proxy_fix (0.1.2)
octokit (5.6.1)
faraday (>= 1, < 3)
sawyer (~> 0.9)
octokit (4.18.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
open4 (1.3.4)
public_suffix (4.0.7)
public_suffix (4.0.3)
rchardet (1.8.0)
redcarpet (3.6.0)
rexml (3.2.5)
rouge (3.30.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
sassc (2.4.0)
redcarpet (3.5.1)
rexml (3.2.4)
rouge (3.17.0)
ruby-macho (1.4.0)
sassc (2.2.1)
ffi (~> 1.9)
sawyer (0.9.2)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
sqlite3 (1.6.2-arm64-darwin)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
typhoeus (1.4.0)
faraday (> 0.8, < 2.0)
sqlite3 (1.4.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.3.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2)
tzinfo (1.2.7)
thread_safe (~> 0.1)
unicode-display_width (1.7.0)
xcinvoke (0.3.0)
liferaft (~> 0.0.6)
xcodeproj (1.22.0)
xcodeproj (1.15.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
nanaimo (~> 0.2.6)
PLATFORMS
arm64-darwin-21
arm64-darwin-22
ruby
DEPENDENCIES
cocoapods
@ -159,4 +153,4 @@ DEPENDENCIES
jazzy
BUNDLED WITH
2.4.12
2.0.2

View File

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

111
Makefile
View File

@ -6,12 +6,21 @@ XCODEFLAGS=-scheme 'swiftlint' \
DSTROOT=$(TEMPORARY_FOLDER) \
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=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint
ARTIFACT_BUNDLE_PATH=$(TEMPORARY_FOLDER)/SwiftLintBinary.artifactbundle
SWIFTLINT_EXECUTABLE=$(shell swift build $(SWIFT_BUILD_FLAGS) --show-bin-path)/swiftlint
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
@ -24,25 +33,25 @@ LICENSE_PATH="$(shell pwd)/LICENSE"
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
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
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Tests/LinuxMain.swift: Tests/*/*.swift .sourcery/LinuxMain.stencil
sourcery --sources Tests --exclude-sources Tests/SwiftLintFrameworkTests/Resources --templates .sourcery/LinuxMain.stencil --output .sourcery --force-parse generated
mv .sourcery/LinuxMain.generated.swift Tests/LinuxMain.swift
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.swift
Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLint*/Rules/**/*.swift .sourcery/GeneratedTests.stencil
./tools/sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/AutomaticRuleTests.stencil
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/AutomaticRuleTests.stencil --output .sourcery
mv .sourcery/AutomaticRuleTests.generated.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
test: clean_xcode
$(BUILD_TOOL) $(XCODEFLAGS) test
@ -52,7 +61,7 @@ test_tsan:
DYLD_INSERT_LIBRARIES=$(TSAN_LIB) $(TSAN_XCTEST) $(TSAN_TEST_BUNDLE)
write_xcodebuild_log:
xcodebuild -scheme swiftlint clean build-for-testing -destination "platform=macOS" > xcodebuild.log
xcodebuild -scheme swiftlint clean build-for-testing > xcodebuild.log
analyze: write_xcodebuild_log
swift run -c release swiftlint analyze --strict --compiler-log-path xcodebuild.log
@ -63,19 +72,14 @@ analyze_autocorrect: write_xcodebuild_log
clean:
rm -f "$(OUTPUT_PACKAGE)"
rm -rf "$(TEMPORARY_FOLDER)"
rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256"
rm -f "./portable_swiftlint.zip"
swift package clean
clean_xcode:
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
build:
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)"
bazel build --config release universal_swiftlint
$(eval SWIFTLINT_BINARY := $(shell bazel cquery --config release --output=files universal_swiftlint))
mv "$(SWIFTLINT_BINARY)" "$(SWIFTLINT_EXECUTABLE)"
chmod +w "$(SWIFTLINT_EXECUTABLE)"
strip -rSTX "$(SWIFTLINT_EXECUTABLE)"
swift build $(SWIFT_BUILD_FLAGS)
build_with_disable_sandbox:
swift build --disable-sandbox $(SWIFT_BUILD_FLAGS)
@ -101,13 +105,6 @@ portable_zip: installables
cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)"
(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
$(eval TMP_FOLDER := $(shell mktemp -d))
docker run swiftlint cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
@ -115,36 +112,24 @@ zip_linux: docker_image
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
zip_linux_release:
$(eval TMP_FOLDER := $(shell mktemp -d))
docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
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)"
package: installables
pkgbuild \
--identifier "io.realm.swiftlint" \
--install-location "/usr/local/bin" \
--root "$(PACKAGE_ROOT)" \
--install-location "/" \
--root "$(TEMPORARY_FOLDER)" \
--version "$(VERSION_STRING)" \
"$(OUTPUT_PACKAGE)"
bazel_release:
bazel build :release
mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 .
release: package portable_zip zip_linux
docker_image:
docker build --platform linux/amd64 --force-rm --tag swiftlint .
docker build --force-rm --tag swiftlint .
docker_test:
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.7-focal swift test --parallel
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.3 swift test --parallel
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
display_compilation_time:
@ -152,8 +137,8 @@ display_compilation_time:
publish:
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
bundle install
bundle exec pod trunk push SwiftLint.podspec
pod trunk push SwiftLintFramework.podspec
pod trunk push SwiftLint.podspec
docs:
swift run swiftlint generate-docs
@ -161,32 +146,20 @@ docs:
bundle exec jazzy
get_version:
@echo "$(VERSION_STRING)"
@echo $(VERSION_STRING)
release:
push_version:
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
$(error git state is not clean)
endif
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
@sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintCore/Models/Version.swift
@sed -e '3s/.*/ version = "$(NEW_VERSION)",/' -i '' MODULE.bazel
make clean
make package
make bazel_release
make portable_zip
make spm_artifactbundle_macos
./tools/update-artifact-bundle.sh "$(NEW_VERSION)"
@sed -i '' 's/## Master/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
@sed 's/__VERSION__/$(NEW_VERSION)/g' script/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
git commit -a -m "release $(NEW_VERSION)"
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
git push origin HEAD
git push origin master
git push origin $(NEW_VERSION)
./tools/create-github-release.sh "$(NEW_VERSION)"
make publish
./tools/add-new-changelog-section.sh
git commit -a -m "Add new changelog section"
git push origin HEAD
%:
@:

View File

@ -1,77 +1,52 @@
{
"pins" : [
{
"identity" : "collectionconcurrencykit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
"state" : {
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version" : "0.2.0"
"object": {
"pins": [
{
"package": "SourceKitten",
"repositoryURL": "https://github.com/jpsim/SourceKitten.git",
"state": {
"branch": null,
"revision": "7f4be006fe73211b0fd9666c73dc2f2303ffa756",
"version": "0.31.0"
}
},
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser.git",
"state": {
"branch": null,
"revision": "92646c0cdbaca076c8d3d0207891785b3379cbff",
"version": "0.3.1"
}
},
{
"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",
"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
]
},
"version": 1
}

View File

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

View File

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

View File

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

370
README.md
View File

@ -1,14 +1,14 @@
# SwiftLint
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide).
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Ray Wenderlich's Swift Style Guide](https://github.com/raywenderlich/swift-style-guide).
SwiftLint hooks into [Clang](http://clang.llvm.org) and
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
[AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation
of your source files for more accurate results.
[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=main)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=main)](https://codecov.io/github/realm/SwiftLint?branch=main)
[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=master)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=master)](https://codecov.io/github/realm/SwiftLint?branch=master)
![](assets/screenshot.png)
@ -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
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
@ -41,7 +41,7 @@ in your Script Build Phases.
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).
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
git is discouraged.
@ -60,74 +60,7 @@ running it.
### Installing from source:
You can also build and install from source by cloning this project and running
`make install` (Xcode 13.3 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
```
`make install` (Xcode 12 or later).
## Usage
@ -136,54 +69,36 @@ bazel run -c opt @SwiftLint//:swiftlint
To get a high-level overview of recommended ways to integrate SwiftLint into your project,
we encourage you to watch this presentation or read the transcript:
[![Presentation](assets/presentation.svg)](https://youtu.be/9Z1nTMTejqU)
[![Presentation](assets/presentation.svg)](https://academy.realm.io/posts/slug-jp-simard-swiftlint/)
### Xcode
Integrate SwiftLint into your Xcode project to get warnings and errors displayed
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".
Insert the following as the script:
![](assets/runscript.png)
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
if [[ "$(uname -m)" == arm64 ]]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
if which swiftlint > /dev/null; then
if which swiftlint >/dev/null; then
swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
```
or you can create a symbolic link in `/usr/local/bin` pointing to the actual binary:
![](assets/runscript.png)
```bash
ln -s /opt/homebrew/bin/swiftlint /usr/local/bin/swiftlint
```
You might want to move your SwiftLint phase directly before the 'Compile Sources'
step to detect errors quickly before compiling. However, SwiftLint is designed
You might want to move your SwiftLint phase directly before 'Compile Sources'
step, to detect errors quickly before compiling. However, SwiftLint is designed
to run on valid Swift code that cleanly completes the compiler's parsing stage.
So running SwiftLint before 'Compile Sources' might yield some incorrect
results.
If you wish to fix violations as well, your script could run
`swiftlint --fix && swiftlint` instead of just `swiftlint`. This will mean
that all correctable violations are fixed while ensuring warnings show up in
If you wish to autocorrect violations as well, your script could run
`swiftlint autocorrect && swiftlint` instead of just `swiftlint`. This will mean
that all correctable violations are fixed, while ensuring warnings show up in
your project for remaining violations.
If you've installed SwiftLint via CocoaPods the script should look like this:
@ -192,57 +107,26 @@ If you've installed SwiftLint via CocoaPods the script should look like this:
"${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
Swift packages.
To run `swiftlint autocorrect` on save in Xcode, install the
[SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) plugin from Alcatraz.
> Due to limitations with Swift Package Manager Plug-ins this is only
recommended for projects that have a SwiftLint configuration in their root directory as
there is currently no way to pass any additional options to the SwiftLint executable.
This plugin will not work with Xcode 8 or later without disabling SIP.
This is not recommended.
#### Xcode
### AppCode
You can integrate SwiftLint as an Xcode Build Tool Plug-in if you're working
with a project in Xcode.
To integrate SwiftLint with AppCode, install
[this plugin](https://plugins.jetbrains.com/plugin/9175) and configure
SwiftLint's installed path in the plugin's preferences.
The `autocorrect` action is available via `⌥⏎`.
Add SwiftLint as a package dependency to your project without linking any of the
products.
### Atom
Select the target you want to add linting to and open the `Build Phases` inspector.
Open `Run Build Tool Plug-ins` and select the `+` button.
Select `SwiftLintPlugin` from the list and add it to the project.
![](assets/select-swiftlint-plugin.png)
For unattended use (e.g. on CI), you can disable the package validation dialog by
* individually passing `-skipPackagePluginValidation` to `xcodebuild` or
* globally setting `defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES`
for that user.
_Note: This implicitly trusts all Xcode package plugins and bypasses Xcode's package validation
dialogs, which has security implications._
#### Swift Package
You can integrate SwiftLint as a Swift Package Manager Plug-in if you're working with
a Swift Package with a `Package.swift` manifest.
Add SwiftLint as a package dependency to your `Package.swift` file.
Add SwiftLint to a target using the `plugins` parameter.
```swift
.target(
...
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
),
```
### Visual Studio Code
To integrate SwiftLint with [vscode](https://code.visualstudio.com), install the
[`vscode-swiftlint`](https://marketplace.visualstudio.com/items?itemName=vknabel.vscode-swiftlint) extension from the marketplace.
To integrate SwiftLint with [Atom](https://atom.io/), install the
[`linter-swiftlint`](https://atom.io/packages/linter-swiftlint) package from
APM.
### fastlane
@ -266,65 +150,32 @@ swiftlint(
)
```
### Docker
`swiftlint` is also available as a [Docker](https://www.docker.com/) image using `Ubuntu`.
So just the first time you need to pull the docker image using the next command:
```bash
docker pull ghcr.io/realm/swiftlint:latest
```
Then following times, you just run `swiftlint` inside of the docker like:
```bash
docker run -it -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest
```
This will execute `swiftlint` in the folder where you are right now (`pwd`), showing an output like:
```bash
$ docker run -it -v `pwd`:`pwd` -w `pwd` ghcr.io/realm/swiftlint:latest
Linting Swift files in current working directory
Linting 'RuleDocumentation.swift' (1/490)
...
Linting 'YamlSwiftLintTests.swift' (490/490)
Done linting! Found 0 violations, 0 serious in 490 files.
```
Here you have more documentation about the usage of [Docker Images](https://docs.docker.com/).
### Command Line
```
$ swiftlint help
OVERVIEW: A tool to enforce Swift style and conventions.
Available commands:
USAGE: swiftlint <subcommand>
OPTIONS:
--version Show the version.
-h, --help Show help information.
SUBCOMMANDS:
analyze Run analysis rules
docs Open SwiftLint documentation website in the default web browser
generate-docs Generates markdown documentation for all rules
lint (default) Print lint warnings and errors
reporters Display the list of reporters and their identifiers
rules Display the list of rules and their identifiers
version Display the current version of SwiftLint
See 'swiftlint help <subcommand>' for detailed help.
analyze [Experimental] Run analysis rules
autocorrect Automatically correct warnings and errors
generate-docs Generates markdown documentation for all rules
help Display general or command-specific help
lint Print lint warnings and errors (default command)
rules Display the list of rules and their identifiers
version Display the current version of SwiftLint
```
Run `swiftlint` in the directory containing the Swift files to lint. Directories
will be searched recursively.
To specify a list of files when using `lint` or `analyze`
To specify a list of files when using `lint`, `autocorrect` or `analyze`
(like the list of files modified by Xcode specified by the
[`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode
plugin, or modified files in the working tree based on `git ls-files -m`), you
can do so by passing the option `--use-script-input-files` and setting the
following instance variables: `SCRIPT_INPUT_FILE_COUNT` and
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}`.
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}`.
These are same environment variables set for input files to
[custom Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/).
@ -360,48 +211,23 @@ You may also set the `TOOLCHAINS` environment variable to the reverse-DNS
notation that identifies a Swift toolchain version:
```shell
$ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint --fix
$ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
```
On Linux, SourceKit is expected to be located in
`/usr/lib/libsourcekitdInProc.so` or specified by the `LINUX_SOURCEKIT_LIB_PATH`
environment variable.
### pre-commit
SwiftLint can be run as a [pre-commit](https://pre-commit.com/) hook.
Once [installed](https://pre-commit.com/#install), add this to the
`.pre-commit-config.yaml` in the root of your repository:
```yaml
repos:
- repo: https://github.com/realm/SwiftLint
rev: 0.50.3
hooks:
- id: swiftlint
```
Adjust `rev` to the SwiftLint version of your choice. `pre-commit autoupdate` can be used to update to the current version.
SwiftLint can be configured using `entry` to apply fixes and fail on errors:
```yaml
- repo: https://github.com/realm/SwiftLint
rev: 0.50.3
hooks:
- id: swiftlint
entry: swiftlint --fix --strict
```
## Rules
Over 200 rules are included in SwiftLint and the Swift community (that's you!)
Over 100 rules are included in SwiftLint and the Swift community (that's you!)
continues to contribute more over time.
[Pull requests](CONTRIBUTING.md) are encouraged.
You can find an updated list of rules and more information about them
[here](https://realm.github.io/SwiftLint/rule-directory.html).
You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules)
You can also check [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)
directory to see their implementation.
### Opt-In Rules
@ -479,9 +305,7 @@ run SwiftLint from. The following parameters can be configured:
Rule inclusion:
* `disabled_rules`: Disable rules from the default enabled set.
* `opt_in_rules`: Enable rules that are not part of the default set. The
special `all` identifier will enable all opt in linter rules, except the ones
listed in `disabled_rules`.
* `opt_in_rules`: Enable rules not from the default set.
* `only_rules`: Only the rules specified in this list will be enabled.
Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
* `analyzer_rules`: This is an entirely separate list of rules that are only
@ -503,9 +327,6 @@ opt_in_rules: # some rules are turned off by default, so you need to opt-in
# - empty_parameters
# - vertical_whitespace
analyzer_rules: # Rules run by `swiftlint analyze`
- explicit_self
included: # paths to include during linting. `--path` is ignored if present.
- Source
excluded: # paths to ignore during linting. Takes precedence over `included`.
@ -514,9 +335,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Source/ExcludedFolder
- Source/ExcludedFile.swift
- Source/*/ExcludedFile.swift # Exclude files with a wildcard
# If true, SwiftLint will not fail if no lintable files are found.
allow_zero_lintable_files: false
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
- explicit_self
# configurable rules can be customized from this configuration file
# binary rules can set their severity level
@ -550,29 +370,13 @@ identifier_name:
- id
- URL
- GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary)
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
```
You can also use environment variables in your configuration file,
by using `${SOME_VARIABLE}` in a string.
### Defining Custom Rules
In addition to the rules that the main SwiftLint project ships with, SwiftLint
can also run two types of custom rules that you can define yourself in your own
projects:
#### 1. Swift Custom Rules
These rules are written the same way as the Swift-based rules that ship with
SwiftLint so they're fast, accurate, can leverage SwiftSyntax, can be unit
tested, and more.
Using these requires building SwiftLint with Bazel as described in
[this video](https://vimeo.com/820572803) or its associated code in
[github.com/jpsim/swiftlint-bazel-example](https://github.com/jpsim/swiftlint-bazel-example).
#### 2. Regex Custom Rules
#### Defining Custom Rules
You can define custom regex-based rules in your configuration file using the
following syntax:
@ -580,10 +384,8 @@ following syntax:
```yaml
custom_rules:
pirates_beat_ninjas: # rule identifier
included:
- ".*\\.swift" # regex that defines paths to include during linting. optional.
excluded:
- ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
included: ".*\\.swift" # regex that defines paths to include during linting. optional.
excluded: ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
name: "Pirates Beat Ninjas" # rule name. optional.
regex: "([nN]inja)" # matching pattern
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
@ -605,68 +407,56 @@ 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
are all the possible syntax kinds:
* `argument`
* `attribute.builtin`
* `attribute.id`
* `buildconfig.id`
* `buildconfig.keyword`
* `comment`
* `comment.mark`
* `comment.url`
* `doccomment`
* `doccomment.field`
* `identifier`
* `keyword`
* `number`
* `objectliteral`
* `parameter`
* `placeholder`
* `string`
* `string_interpolation_anchor`
* `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.
* argument
* attribute.builtin
* attribute.id
* buildconfig.id
* buildconfig.keyword
* comment
* comment.mark
* comment.url
* doccomment
* doccomment.field
* identifier
* keyword
* number
* objectliteral
* parameter
* placeholder
* string
* string_interpolation_anchor
* typeidentifier
If using custom rules in combination with `only_rules`, make sure to add
`custom_rules` as an item under `only_rules`.
Unlike Swift custom rules, you can use official SwiftLint builds
(e.g. from Homebrew) to run regex custom rules.
### Auto-correct
SwiftLint can automatically correct certain violations. Files on disk are
overwritten with a corrected version.
Please make sure to have backups of these files before running
`swiftlint --fix`, otherwise important data may be lost.
`swiftlint autocorrect`, otherwise important data may be lost.
Standard linting is disabled while correcting because of the high likelihood of
violations (or their offsets) being incorrect after modifying a file while
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
command invocation (incremental builds will fail) must be passed to `analyze`
via the `--compiler-log-path` flag.
e.g. `--compiler-log-path /path/to/xcodebuild.log`
This can be obtained by
This can be obtained by running
`xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log`
with a clean `DerivedData` folder.
1. Cleaning DerivedData (incremental builds won't work with analyze)
2. Running `xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > 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
@ -676,7 +466,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:
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.
Team-Wide Configuration:
@ -755,7 +545,7 @@ while the last one is treated as the highest-priority child.
A simple example including just two configuration files looks like this:
`swiftlint --config .swiftlint.yml --config .swiftlint_child.yml`
`swiftlint --config ".swiftlint.yml .swiftlint_child.yml"`
### Nested Configurations
@ -775,7 +565,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,
that configuration will be treated as an override, no matter whether there exist
other `.swiftlint.yml` files somewhere within the directory. **So if you want to use
nested configurations, you can't use the `--config` parameter.**
use nested configurations, you can't use the `-- config` parameter.**
## License

View File

@ -1,11 +1,11 @@
# 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) 来表示源代码文件的更多精确结果。
[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=main)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=main)](https://codecov.io/github/realm/SwiftLint?branch=main)
[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=master)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=master)](https://codecov.io/github/realm/SwiftLint?branch=master)
![](assets/screenshot.png)
@ -45,7 +45,7 @@ $ mint install realm/SwiftLint
### 编译源代码:
你也可以通过 Clone SwiftLint 的 Git 仓库到本地然后执行 `make install` (Xcode 12.5+) 编译源代码的方式来安装。
你也可以通过 Clone SwiftLint 的 Git 仓库到本地然后执行 `make install` (Xcode 11.4+) 编译源代码的方式来安装。
## 用法
@ -127,7 +127,7 @@ Available commands:
在包含有需要执行代码分析的 Swift 源码文件的目录下执行 `swiftlint` 命令,会对目录进行递归查找。
当使用 `lint` 或者 `autocorrect` 命令时,你可以通过添加 `--use-script-input-files` 选项并且设置以下实例变量:`SCRIPT_INPUT_FILE_COUNT` 和
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`... `SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}` 的方式来指定一个文件列表(就像被 Xcode 特别是 [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode 插件修改的文件组成的列表,或者类似 Git 工作树中 `git ls-files -m` 命令显示的被修改的文件列表)。
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`... `SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}` 的方式来指定一个文件列表(就像被 Xcode 特别是 [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode 插件修改的文件组成的列表,或者类似 Git 工作树中 `git ls-files -m` 命令显示的被修改的文件列表)。
也有类似的用来设置输入文件的环境变量以 [自定义 Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/) 。
@ -167,7 +167,7 @@ SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
你也可以检视 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules) 目录来查看它们的实现。
你也可以检视 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules) 目录来查看它们的实现。
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。

View File

@ -1,11 +1,11 @@
# 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) 표현을 사용합니다.
[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=main)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=main)
[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=main)](https://codecov.io/github/realm/SwiftLint?branch=main)
[![Build Status](https://dev.azure.com/jpsim/SwiftLint/_apis/build/status/realm.SwiftLint?branchName=master)](https://dev.azure.com/jpsim/SwiftLint/_build/latest?definitionId=4?branchName=master)
[![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=master)](https://codecov.io/github/realm/SwiftLint?branch=master)
![](assets/screenshot.png)
@ -27,11 +27,11 @@ Podfile에 아래 라인을 추가하기만 하면 됩니다.
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는 최신 버전만 설치 가능)
이렇게 했을 때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉터리에 추가되기 때문에, git 등의 SCM에 이런 디렉터리들을 체크인하는 것은 권장하지 않습니다.
이렇게 했을때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉토리에 추가되기 때문에, git 등의 SCM에 이런 디렉토리들을 체크인하는 것은 권장하지 않습니다.
### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우:
```
@ -44,7 +44,7 @@ $ mint install realm/SwiftLint
### 소스를 직접 컴파일하는 경우:
본 프로젝트를 클론해서 빌드할 수도 있습니다. `make install` 명령을 사용합니다. (Xcode 12.5 이후 버전)
본 프로젝트를 클론해서 빌드할 수도 있습니다. `make install` 명령을 사용합니다. (Xcode 11.4 이후 버전)
## 사용 방법
@ -56,7 +56,7 @@ $ mint install realm/SwiftLint
### Xcode
SwiftLint를 Xcode 프로젝트에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. 프로젝트의 파일 내비게이터에서 타겟 앱을 선택 후 "Build Phases" 탭으로 이동합니다. + 버튼을 클릭한 후 "Run Script Phase"를 선택합니다. 그 후 아래 스크립트를 추가하기만 하면 됩니다.
SwiftLint를 Xcode 스킴에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. "Run Script Phase"를 새로 만들고 아래 스크립트를 추가하기만 하면 됩니다.
```bash
if which swiftlint >/dev/null; then
@ -68,34 +68,6 @@ fi
![](assets/runscript.png)
만약, 애플 실리콘 환경에서 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를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다.
```bash
@ -144,10 +116,10 @@ Available commands:
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
`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}`
이는 [Xcode의 커스텀 스크립트 단계](http://indiestack.com/2014/12/speeding-up-custom-script-phases/)에 입력 파일로 환경 변수를 지정하는 것과 동일합니다.
@ -171,7 +143,7 @@ SwiftLint가 어느 스위프트 툴체인을 사용할지 결정하는 순서
* `~/Applications/Xcode.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 형식으로 지정할 수도 있습니다.
@ -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`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
@ -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
custom_rules:
pirates_beat_ninjas: # 룰 식별자
included: ".*.swift" # 린트 실행 시 포함할 경로를 정의하는 정규표현식. 선택 가능.
included: ".*.swift" # 린트 실행시 포함할 경로를 정의하는 정규표현식. 선택 가능.
name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능.
regex: "([nN]inja)" # 패턴 매칭
match_kinds: # 매칭할 SyntaxKinds. 선택 가능.
@ -339,8 +311,8 @@ custom_rules:
SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다.
* 디렉리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
* 각 파일은 자신의 디렉터리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉터리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
* 디렉리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
* 각 파일은 자신의 디렉토리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉토리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
* 중첩 구성에서 `excluded``included`는 무시됩니다.
### 자동 수정

View File

@ -7,8 +7,17 @@ For SwiftLint contributors, follow these steps to cut a release:
* FabricSoftenerRule
* Top Loading
* Fresh Out Of The Dryer
1. Push new version: `make push_version "0.2.0: Tumble Dry"`
1. Make sure you have the latest stable Xcode version installed and
`xcode-select`ed
1. Release new version: `make release "0.2.0: Tumble Dry"`
1. Wait for the Docker CI job to finish then run: `make zip_linux_release`
`xcode-select`ed.
1. Create the pkg installer, framework zip, portable zip, and Linux zip:
`make release`
1. Create a GitHub release: https://github.com/realm/SwiftLint/releases/new
* Specify the tag you just pushed from the dropdown.
* Set the release title to the new version number & release name.
* Add the changelog section to the release description text box.
* Upload the 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:

View File

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

View File

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

View File

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

View File

@ -1,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)
}
}

View File

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

View File

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

View File

@ -1,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)
}
}
}
}

View File

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

View File

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

View File

@ -1,237 +0,0 @@
import SwiftSyntax
struct DiscouragedNoneNameRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "discouraged_none_name",
name: "Discouraged None Name",
description: "Enum cases and static members named `none` are discouraged as they can conflict with " +
"`Optional<T>.none`.",
kind: .idiomatic,
nonTriggeringExamples: [
// Should not trigger unless exactly matches "none"
Example("""
enum MyEnum {
case nOne
}
"""),
Example("""
enum MyEnum {
case _none
}
"""),
Example("""
enum MyEnum {
case none_
}
"""),
Example("""
enum MyEnum {
case none(Any)
}
"""),
Example("""
enum MyEnum {
case nonenone
}
"""),
Example("""
class MyClass {
class var nonenone: MyClass { MyClass() }
}
"""),
Example("""
class MyClass {
static var nonenone = MyClass()
}
"""),
Example("""
class MyClass {
static let nonenone = MyClass()
}
"""),
Example("""
struct MyStruct {
static var nonenone = MyStruct()
}
"""),
Example("""
struct MyStruct {
static let nonenone = MyStruct()
}
"""),
// Should not trigger if not an enum case or static/class member
Example("""
struct MyStruct {
let none = MyStruct()
}
"""),
Example("""
struct MyStruct {
var none = MyStruct()
}
"""),
Example("""
class MyClass {
let none = MyClass()
}
"""),
Example("""
class MyClass {
var none = MyClass()
}
""")
],
triggeringExamples: [
Example("""
enum MyEnum {
case none
}
"""),
Example("""
enum MyEnum {
case a, none
}
"""),
Example("""
enum MyEnum {
case none, b
}
"""),
Example("""
enum MyEnum {
case a, none, b
}
"""),
Example("""
enum MyEnum {
case a
case none
}
"""),
Example("""
enum MyEnum {
case none
case b
}
"""),
Example("""
enum MyEnum {
case a
case none
case b
}
"""),
Example("""
class MyClass {
static let none = MyClass()
}
"""),
Example("""
class MyClass {
static let none: MyClass = MyClass()
}
"""),
Example("""
class MyClass {
static var none: MyClass = MyClass()
}
"""),
Example("""
class MyClass {
class var none: MyClass { MyClass() }
}
"""),
Example("""
struct MyStruct {
static var none = MyStruct()
}
"""),
Example("""
struct MyStruct {
static var none: MyStruct = MyStruct()
}
"""),
Example("""
struct MyStruct {
static var none = MyStruct()
}
"""),
Example("""
struct MyStruct {
static var none: MyStruct = MyStruct()
}
"""),
Example("""
struct MyStruct {
static var a = MyStruct(), none = MyStruct()
}
"""),
Example("""
struct MyStruct {
static var none = MyStruct(), a = MyStruct()
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension DiscouragedNoneNameRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: EnumCaseElementSyntax) {
let emptyParams = node.associatedValue?.parameterList.isEmpty ?? true
if emptyParams, node.identifier.isNone {
violations.append(ReasonedRuleViolation(
position: node.positionAfterSkippingLeadingTrivia,
reason: reason(type: "`case`")
))
}
}
override func visitPost(_ node: VariableDeclSyntax) {
let type: String? = {
if node.modifiers.isClass {
return "`class` member"
} else if node.modifiers.isStatic {
return "`static` member"
} else {
return nil
}
}()
guard let type else {
return
}
for binding in node.bindings {
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self), pattern.identifier.isNone else {
continue
}
violations.append(ReasonedRuleViolation(
position: node.positionAfterSkippingLeadingTrivia,
reason: reason(type: type)
))
return
}
}
private func reason(type: String) -> String {
let reason = "Avoid naming \(type) `none` as the compiler can think you mean `Optional<T>.none`"
let recommendation = "consider using an Optional value instead"
return "\(reason); \(recommendation)"
}
}
}
private extension TokenSyntax {
var isNone: Bool {
tokenKind == .identifier("none") || tokenKind == .identifier("`none`")
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
)
}
}
}

View File

@ -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
}()
}

View File

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

View File

@ -1,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 }
}

View File

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

View File

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

View File

@ -1,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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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)
}
}
}
}

View File

@ -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)
}
}

View File

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

View File

@ -1,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
)
)
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,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("??")
}
}

View File

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

View File

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

View File

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

View File

@ -1,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"
}
}
}

View File

@ -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
}
}

View File

@ -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()
}
""")
]
}

View File

@ -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
}
}

View File

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

View File

@ -1,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 }
}

View File

@ -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]?")
]
}

View File

@ -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
}
}

View File

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

View File

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

View File

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

View File

@ -1,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.")
}
}
}

View File

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

View File

@ -1,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)
}
}

View File

@ -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([]))
)
}
}

View File

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

View File

@ -1,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
}
}
}

View File

@ -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
}
}

View File

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

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