Compare commits

..

1 Commits

Author SHA1 Message Date
Marcelo Fabri aeef110410 WIP 2020-02-10 21:40:38 -08:00
927 changed files with 29546 additions and 48002 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 +0,0 @@
*
!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 }}

20
.gitignore vendored
View File

@ -1,5 +1,6 @@
# Xcode # Xcode
# #
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated ## Build generated
build/ build/
@ -21,7 +22,6 @@ xcuserdata
*.moved-aside *.moved-aside
*.xcuserstate *.xcuserstate
*.xcscmblueprint *.xcscmblueprint
default.profraw
## Obj-C/Swift specific ## Obj-C/Swift specific
*.hmap *.hmap
@ -35,19 +35,22 @@ default.profraw
# #
#Pods/ #Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# SwiftLint # SwiftLint
SwiftLint.xcodeproj
SwiftLint.pkg SwiftLint.pkg
*.zip SwiftLintFramework.framework.zip
benchmark_* benchmark_*
portable_swiftlint.zip
osscheck/ osscheck/
docs/ docs/
rule_docs/ rule_docs/
bazel.tar.gz
bazel.tar.gz.sha256
ci.bazelrc
user.bazelrc
# Swift Package Manager # Swift Package Manager
# #
@ -62,6 +65,3 @@ Packages/
# Bundler # Bundler
.bundle/ .bundle/
bundle/ bundle/
# Bazel
bazel-*

21
.gitmodules vendored Normal file
View File

@ -0,0 +1,21 @@
[submodule "Carthage/Checkouts/SWXMLHash"]
path = Carthage/Checkouts/SWXMLHash
url = https://github.com/drmohundro/SWXMLHash.git
[submodule "Carthage/Checkouts/xcconfigs"]
path = Carthage/Checkouts/xcconfigs
url = https://github.com/jspahrsummers/xcconfigs.git
[submodule "Carthage/Checkouts/Commandant"]
path = Carthage/Checkouts/Commandant
url = https://github.com/Carthage/Commandant.git
[submodule "Carthage/Checkouts/SourceKitten"]
path = Carthage/Checkouts/SourceKitten
url = https://github.com/jpsim/SourceKitten.git
[submodule "Carthage/Checkouts/SwiftyTextTable"]
path = Carthage/Checkouts/SwiftyTextTable
url = https://github.com/scottrhoyt/SwiftyTextTable.git
[submodule "Carthage/Checkouts/Yams"]
path = Carthage/Checkouts/Yams
url = https://github.com/jpsim/Yams.git
[submodule "SwiftSyntax"]
path = SwiftSyntax
url = https://github.com/apple/swift-syntax.git

View File

@ -1,20 +1,19 @@
module: SwiftLintCore module: SwiftLintFramework
author: JP Simard, SwiftLint Contributors author: JP Simard, SwiftLint Contributors
author_url: https://jpsim.com author_url: https://jpsim.com
root_url: https://realm.github.io/SwiftLint/ root_url: https://realm.github.io/SwiftLint/
github_url: https://github.com/realm/SwiftLint github_url: https://github.com/realm/SwiftLint
github_file_prefix: https://github.com/realm/SwiftLint/tree/main github_file_prefix: https://github.com/realm/SwiftLint/tree/master
swift_build_tool: spm swift_build_tool: spm
theme: fullwidth theme: fullwidth
clean: true clean: true
copyright: '© 2023 [JP Simard](https://jpsim.com) under MIT.' copyright: '© 2020 [JP Simard](https://jpsim.com) under MIT.'
documentation: rule_docs/*.md documentation: rule_docs/*.md
hide_unlisted_documentation: true hide_unlisted_documentation: true
custom_categories_unlisted_prefix: '' custom_categories_unlisted_prefix: ''
exclude: exclude:
# TODO: Document extensions - Source/SwiftLintFramework/Rules/**/*.swift
- Source/SwiftLintCore/Extensions/*.swift
custom_categories: custom_categories:
- name: Rules - name: Rules
children: children:
@ -23,7 +22,6 @@ custom_categories:
children: children:
- CSVReporter - CSVReporter
- CheckstyleReporter - CheckstyleReporter
- CodeClimateReporter
- EmojiReporter - EmojiReporter
- GitHubActionsLoggingReporter - GitHubActionsLoggingReporter
- HTMLReporter - 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. /// The rule list containing all available rules built into SwiftLint.
public let builtInRules: [Rule.Type] = [ public let masterRuleList = RuleList(rules: [
{% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %} {% for rule in types.structs where rule.name|hasSuffix:"Rule" or rule.name|hasSuffix:"Rules" %} {{ rule.name }}.self{% if not forloop.last %},{% endif %}
{% endfor %}] {% endfor %}])

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

1
.swift-version Normal file
View File

@ -0,0 +1 @@
5.0

View File

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

211
BUILD
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,47 +1,21 @@
## Tutorial
If you'd like to write a SwiftLint rule but aren't sure how to start,
please watch and follow along with
[this video tutorial](https://vimeo.com/819268038).
## Pull Requests ## Pull Requests
All changes, no matter how trivial, must be done via pull request. Commits All changes, no matter how trivial, must be done via pull request. Commits
should never be made directly on the `main` branch. Prefer rebasing over should never be made directly on the `master` branch. Prefer rebasing over
merging `main` into your PR branch to update it and resolve conflicts. merging `master` into your PR branch to update it and resolve conflicts.
_If you have commit access to SwiftLint and believe your change to be trivial _If you have commit access to SwiftLint and believe your change to be trivial
and not worth waiting for review, you may open a pull request and merge and not worth waiting for review, you may open a pull request and merge
immediately, but this should be the exception, not the norm._ immediately, but this should be the exception, not the norm._
### Building And Running Locally ### Submodules
#### Using Xcode This SwiftLint repository uses submodules for its dependencies.
This means that if you decide to fork this repository to contribute to SwiftLint,
don't forget to checkout the submodules as well when cloning, by running
`git submodule update --init --recursive` after cloning.
1. `git clone https://github.com/realm/SwiftLint.git` See more info [in the README](https://github.com/realm/SwiftLint#installation).
1. `cd SwiftLint`
1. `xed .`
1. Select the "swiftlint" scheme
1. `cmd-opt-r` open the scheme options
1. Set the "Arguments Passed On Launch" you want in the "Arguments" tab. See
available arguments [in the README](https://github.com/realm/SwiftLint#command-line).
1. Set the "Working Directory" in the "Options" tab to the path where you would like
to execute SwiftLint. A folder that contains swift source files.
1. Hit "Run"
|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)|
Then you can use the full power of Xcode/LLDB/Instruments to develop and debug your changes to SwiftLint.
#### Using the command line
1. `git clone https://github.com/realm/SwiftLint.git`
1. `cd SwiftLint`
1. `swift build [-c release]`
1. Use the produced `swiftlint` binary from the command line, either by running `swift run [-c release] [swiftlint] [arguments]` or by invoking the binary directly at `.build/[release|debug]/swiftlint`
1. [Optional] Attach LLDB: `lldb -- .build/[release|debug]/swiftlint [arguments]`
### Code Generation ### Code Generation
@ -57,14 +31,14 @@ with Swift Package Manager on Linux. When contributing code changes, please
ensure that all three supported build methods continue to work and pass tests. ensure that all three supported build methods continue to work and pass tests.
```shell ```shell
$ xcodebuild -scheme swiftlint test $ script/cibuild
$ swift test $ swift test
$ make docker_test $ make docker_test
``` ```
## Rules ## Rules
New rules should be added in the `Source/SwiftLintBuiltInRules/Rules` directory. New rules should be added in the `Source/SwiftLintFramework/Rules` directory.
Rules should conform to either the `Rule` or `ASTRule` protocols. Rules should conform to either the `Rule` or `ASTRule` protocols.
@ -78,19 +52,6 @@ over time. This way adding a unit test for your new Rule is just a matter of
adding a test case in `RulesTests.swift` which simply calls adding a test case in `RulesTests.swift` which simply calls
`verifyRule(YourNewRule.description)`. `verifyRule(YourNewRule.description)`.
For debugging purposes examples can be marked as `focused`. If there are any
focused examples found, then only those will be run when running tests for that rule.
```
nonTriggeringExamples: [
Example("let x: [Int]"),
Example("let x: [Int: String]").focused() // only this one will be run in tests
],
triggeringExamples: [
Example("let x: ↓Array<String>"),
Example("let x: ↓Dictionary<Int, String>")
]
```
### `ConfigurationProviderRule` ### `ConfigurationProviderRule`
If your rule supports user-configurable options via `.swiftlint.yml`, you can If your rule supports user-configurable options via `.swiftlint.yml`, you can
@ -104,11 +65,11 @@ configuration object via the `configuration` property:
* If none of the provided `RuleConfiguration`s are applicable, you can create one * If none of the provided `RuleConfiguration`s are applicable, you can create one
specifically for your rule. specifically for your rule.
See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ForceCastRule.swift) See [`ForceCastRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/ForceCastRule.swift)
for a rule that allows severity configuration, for a rule that allows severity configuration,
[`FileLengthRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift) [`FileLengthRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/FileLengthRule.swift)
for a rule that has multiple severity levels, for a rule that has multiple severity levels,
[`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/main/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift) [`IdentifierNameRule`](https://github.com/realm/SwiftLint/blob/master/Source/SwiftLintFramework/Rules/IdentifierNameRule.swift)
for a rule that allows name evaluation configuration: for a rule that allows name evaluation configuration:
``` yaml ``` yaml
@ -145,40 +106,16 @@ If your rule is configurable, but does not fit the pattern of
All changes should be made via pull requests on GitHub. All changes should be made via pull requests on GitHub.
When issuing a pull request with user-facing changes, please add a When issuing a pull request, please add a summary of your changes to
summary of your changes to the `CHANGELOG.md` file. the `CHANGELOG.md` file.
We follow the same syntax as CocoaPods' CHANGELOG.md: We follow the same syntax as CocoaPods' CHANGELOG.md:
1. One Markdown unnumbered list item describing the change. 1. One Markdown unnumbered list item describing the change.
2. 2 trailing spaces on the last line describing the change (so that Markdown renders each change [on its own line](https://daringfireball.net/projects/markdown/syntax#p)). 2. 2 trailing spaces on the last line describing the change.
3. A list of Markdown hyperlinks to the contributors to the change. One entry 3. A list of Markdown hyperlinks to the contributors to the change. One entry
per line. Usually just one. per line. Usually just one.
4. A list of Markdown hyperlinks to the issues the change addresses. One entry 4. A list of Markdown hyperlinks to the issues the change addresses. One entry
per line. Usually just one. If there was no issue tracking this change, per line. Usually just one. If there was no issue tracking this change,
you may instead link to the change's pull request. you may instead link to the change's pull request.
5. All CHANGELOG.md content is hard-wrapped at 80 characters. 5. All CHANGELOG.md content is hard-wrapped at 80 characters.
## CI
SwiftLint uses Azure Pipelines for most of its CI jobs, primarily because
they're the only CI provider to have a free tier with 10x concurrency on macOS.
Some CI jobs run in GitHub Actions (e.g. Docker).
Some CI jobs run on Buildkite using Mac Minis from MacStadium. These are jobs
that benefit from being run on the latest Xcode & macOS versions on bare metal.
### Buildkite Setup
To bring up a new Buildkite worker from MacStadium:
1. Change account password
1. Update macOS to the latest version
1. Install Homebrew: https://brew.sh
1. Install Buildkite agent and other tools via Homebrew:
`brew install aria2 bazelisk htop buildkite/buildkite/buildkite-agent robotsandpencils/made/xcodes`
1. Install latest Xcode version: `xcodes update && xcodes install 14.0.0`
1. Add `DANGER_GITHUB_API_TOKEN` and `HOME` to `/opt/homebrew/etc/buildkite-agent/hooks/environment`
1. Configure and launch buildkite agent: `brew info buildkite-agent` /
https://buildkite.com/organizations/swiftlint/agents#setup-macos

2
Cartfile Normal file
View File

@ -0,0 +1,2 @@
github "jpsim/SourceKitten" ~> 0.29.0
github "scottrhoyt/SwiftyTextTable" ~> 0.9.0

3
Cartfile.private Normal file
View File

@ -0,0 +1,3 @@
github "Carthage/Commandant" ~> 0.17.0
github "jpsim/Yams" ~> 2.0.0
github "jspahrsummers/xcconfigs" ~> 0.12.0

6
Cartfile.resolved Normal file
View File

@ -0,0 +1,6 @@
github "Carthage/Commandant" "0.17.0"
github "drmohundro/SWXMLHash" "5.0.1"
github "jpsim/SourceKitten" "0.29.0"
github "jpsim/Yams" "2.0.0"
github "jspahrsummers/xcconfigs" "0.12"
github "scottrhoyt/SwiftyTextTable" "0.9.0"

1
Carthage/Checkouts/Commandant vendored Submodule

@ -0,0 +1 @@
Subproject commit ab68611013dec67413628ac87c1f29e8427bc8e4

1
Carthage/Checkouts/SWXMLHash vendored Submodule

@ -0,0 +1 @@
Subproject commit a4931e5c3bafbedeb1601d3bb76bbe835c6d475a

1
Carthage/Checkouts/SourceKitten vendored Submodule

@ -0,0 +1 @@
Subproject commit 77a4dbbb477a8110eb8765e3c44c70fb4929098f

1
Carthage/Checkouts/SwiftyTextTable vendored Submodule

@ -0,0 +1 @@
Subproject commit c6df6cf533d120716bff38f8ff9885e1ce2a4ac3

1
Carthage/Checkouts/Yams vendored Submodule

@ -0,0 +1 @@
Subproject commit c947a306d2e80ecb2c0859047b35c73b8e1ca27f

1
Carthage/Checkouts/xcconfigs vendored Submodule

@ -0,0 +1 @@
Subproject commit bb795558a76e5daf3688500055bbcfe243bffa8d

View File

@ -14,13 +14,11 @@ modified_files = git.modified_files + git.added_files
# including in a CHANGELOG for example # including in a CHANGELOG for example
has_app_changes = !modified_files.grep(/Source/).empty? has_app_changes = !modified_files.grep(/Source/).empty?
has_test_changes = !modified_files.grep(/Tests/).empty? has_test_changes = !modified_files.grep(/Tests/).empty?
has_danger_changes = !modified_files.grep(/Dangerfile|tools\/oss-check|Gemfile/).empty? has_danger_changes = !modified_files.grep(/Dangerfile|script\/oss-check|Gemfile/).empty?
has_package_changes = !modified_files.grep(/Package\.swift/).empty?
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD|MODULE\.bazel/).empty?
# Add a CHANGELOG entry for app changes # Add a CHANGELOG entry for app changes
if !modified_files.include?('CHANGELOG.md') && has_app_changes if !modified_files.include?('CHANGELOG.md') && has_app_changes
warn("If this is a user-facing change, please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/realm/SwiftLint/blob/main/CHANGELOG.md).") warn("Please include a CHANGELOG entry to credit yourself! \nYou can find it at [CHANGELOG.md](https://github.com/realm/SwiftLint/blob/master/CHANGELOG.md).")
markdown <<-MARKDOWN markdown <<-MARKDOWN
Here's an example of your CHANGELOG entry: Here's an example of your CHANGELOG entry:
```markdown ```markdown
@ -32,7 +30,7 @@ Here's an example of your CHANGELOG entry:
MARKDOWN MARKDOWN
end end
return unless has_app_changes || has_danger_changes || has_package_changes || has_bazel_changes return unless has_app_changes || has_danger_changes
# Non-trivial amounts of app changes without tests # Non-trivial amounts of app changes without tests
if git.lines_of_code > 50 && has_app_changes && !has_test_changes if git.lines_of_code > 50 && has_app_changes && !has_test_changes
@ -51,8 +49,7 @@ end
file = Tempfile.new('violations') file = Tempfile.new('violations')
force_flag = has_danger_changes ? "--force" : "" Open3.popen3("script/oss-check -v 2> #{file.path}") do |_, stdout, _, _|
Open3.popen3("tools/oss-check -v #{force_flag} 2> #{file.path}") do |_, stdout, _, _|
while char = stdout.getc while char = stdout.getc
print char print char
end end

View File

@ -1,39 +0,0 @@
# Explicitly specify `jammy` to keep the Swift & Ubuntu images in sync.
ARG BUILDER_IMAGE=swift:jammy
ARG RUNTIME_IMAGE=ubuntu:jammy
# builder image
FROM ${BUILDER_IMAGE} AS builder
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
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
RUN mkdir -p /executables
RUN install -v `swift build $SWIFT_FLAGS --show-bin-path`/swiftlint /executables
# runtime image
FROM ${RUNTIME_IMAGE}
LABEL org.opencontainers.image.source https://github.com/realm/SwiftLint
RUN apt-get update && apt-get install -y \
libcurl4 \
libxml2 \
&& rm -r /var/lib/apt/lists/*
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,141 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.6) CFPropertyList (3.0.2)
rexml activesupport (4.2.11.1)
activesupport (7.0.4.3) i18n (~> 0.7)
concurrent-ruby (~> 1.0, >= 1.0.2) minitest (~> 5.1)
i18n (>= 1.6, < 2) thread_safe (~> 0.3, >= 0.3.4)
minitest (>= 5.1) tzinfo (~> 1.1)
tzinfo (~> 2.0) addressable (2.7.0)
addressable (2.8.4) public_suffix (>= 2.0.2, < 5.0)
public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.1)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3) httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1) json (>= 1.5.1)
atomos (0.1.3) atomos (0.1.3)
claide (1.1.0) claide (1.0.3)
claide-plugins (0.9.2) claide-plugins (0.9.2)
cork cork
nap nap
open4 (~> 1.3) open4 (~> 1.3)
cocoapods (1.12.1) cocoapods (1.8.4)
addressable (~> 2.8) activesupport (>= 4.0.2, < 5)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.12.1) cocoapods-core (= 1.8.4)
cocoapods-deintegrate (>= 1.0.3, < 2.0) 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-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.4.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
escape (~> 0.0.4) escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0) fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0) gh_inspector (~> 1.0)
molinillo (~> 0.8.0) molinillo (~> 0.6.6)
nap (~> 1.0) nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0) ruby-macho (~> 1.4)
xcodeproj (>= 1.21.0, < 2.0) xcodeproj (>= 1.11.1, < 2.0)
cocoapods-core (1.12.1) cocoapods-core (1.8.4)
activesupport (>= 5.0, < 8) activesupport (>= 4.0.2, < 6)
addressable (~> 2.8)
algoliasearch (~> 1.0) algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4) fuzzy_match (~> 2.0.4)
nap (~> 1.0) nap (~> 1.0)
netrc (~> 0.11) cocoapods-deintegrate (1.0.4)
public_suffix (~> 4.0) cocoapods-downloader (1.3.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.6.3)
cocoapods-plugins (1.0.0) cocoapods-plugins (1.0.0)
nap nap
cocoapods-search (1.0.1) cocoapods-search (1.0.0)
cocoapods-trunk (1.6.0) cocoapods-stats (1.1.0)
cocoapods-trunk (1.4.1)
nap (>= 0.8, < 2.0) nap (>= 0.8, < 2.0)
netrc (~> 0.11) netrc (~> 0.11)
cocoapods-try (1.2.0) cocoapods-try (1.1.0)
colored2 (3.1.2) colored2 (3.1.2)
concurrent-ruby (1.2.2) concurrent-ruby (1.1.5)
cork (0.3.0) cork (0.3.0)
colored2 (~> 3.1) colored2 (~> 3.1)
danger (9.2.0) danger (6.1.0)
claide (~> 1.0) claide (~> 1.0)
claide-plugins (>= 0.9.2) claide-plugins (>= 0.9.2)
colored2 (~> 3.1) colored2 (~> 3.1)
cork (~> 0.1) cork (~> 0.1)
faraday (>= 0.9.0, < 3.0) faraday (~> 0.9)
faraday-http-cache (~> 2.0) faraday-http-cache (~> 2.0)
git (~> 1.7) git (~> 1.5)
kramdown (~> 2.3) kramdown (~> 2.0)
kramdown-parser-gfm (~> 1.0) kramdown-parser-gfm (~> 1.0)
no_proxy_fix no_proxy_fix
octokit (~> 5.0) octokit (~> 4.7)
terminal-table (>= 1, < 4) terminal-table (~> 1)
escape (0.0.4) escape (0.0.4)
ethon (0.16.0) faraday (0.17.3)
ffi (>= 1.15.0) multipart-post (>= 1.2, < 3)
faraday (2.7.4) faraday-http-cache (2.0.0)
faraday-net_http (>= 2.0, < 3.1) faraday (~> 0.8)
ruby2_keywords (>= 0.0.4) ffi (1.11.3)
faraday-http-cache (2.4.1)
faraday (>= 0.8)
faraday-net_http (3.0.2)
ffi (1.15.5)
fourflusher (2.3.1) fourflusher (2.3.1)
fuzzy_match (2.0.4) fuzzy_match (2.0.4)
gh_inspector (1.1.3) gh_inspector (1.1.3)
git (1.18.0) git (1.5.0)
addressable (~> 2.8)
rchardet (~> 1.8)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.12.0) i18n (0.9.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jazzy (0.14.3) jazzy (0.13.1)
cocoapods (~> 1.5) cocoapods (~> 1.5)
mustache (~> 1.1) mustache (~> 1.1)
open4 (~> 1.3) open4
redcarpet (~> 3.4) redcarpet (~> 3.4)
rexml (~> 3.2)
rouge (>= 2.0.6, < 4.0) rouge (>= 2.0.6, < 4.0)
sassc (~> 2.1) sassc (~> 2.1)
sqlite3 (~> 1.3) sqlite3 (~> 1.3)
xcinvoke (~> 0.3.0) xcinvoke (~> 0.3.0)
json (2.6.3) json (2.3.0)
kramdown (2.4.0) kramdown (2.1.0)
rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
liferaft (0.0.6) liferaft (0.0.6)
minitest (5.18.0) minitest (5.13.0)
molinillo (0.8.0) molinillo (0.6.6)
multipart-post (2.1.1)
mustache (1.1.1) mustache (1.1.1)
nanaimo (0.3.0) nanaimo (0.2.6)
nap (1.1.0) nap (1.1.0)
netrc (0.11.0) netrc (0.11.0)
no_proxy_fix (0.1.2) no_proxy_fix (0.1.2)
octokit (5.6.1) octokit (4.15.0)
faraday (>= 1, < 3) faraday (>= 0.9)
sawyer (~> 0.9) sawyer (~> 0.8.0, >= 0.5.3)
open4 (1.3.4) open4 (1.3.4)
public_suffix (4.0.7) public_suffix (4.0.3)
rchardet (1.8.0) redcarpet (3.5.0)
redcarpet (3.6.0) rouge (3.14.0)
rexml (3.2.5) ruby-macho (1.4.0)
rouge (3.30.0) sassc (2.2.1)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
sassc (2.4.0)
ffi (~> 1.9) ffi (~> 1.9)
sawyer (0.9.2) sawyer (0.8.2)
addressable (>= 2.3.5) addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3) faraday (> 0.8, < 2.0)
sqlite3 (1.6.2-arm64-darwin) sqlite3 (1.4.2)
terminal-table (3.0.2) terminal-table (1.8.0)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (~> 1.1, >= 1.1.1)
typhoeus (1.4.0) thread_safe (0.3.6)
ethon (>= 0.9.0) tzinfo (1.2.6)
tzinfo (2.0.6) thread_safe (~> 0.1)
concurrent-ruby (~> 1.0) unicode-display_width (1.6.0)
unicode-display_width (2.4.2)
xcinvoke (0.3.0) xcinvoke (0.3.0)
liferaft (~> 0.0.6) liferaft (~> 0.0.6)
xcodeproj (1.22.0) xcodeproj (1.14.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.2.6)
rexml (~> 3.2.4)
PLATFORMS PLATFORMS
arm64-darwin-21 ruby
arm64-darwin-22
DEPENDENCIES DEPENDENCIES
cocoapods cocoapods
@ -159,4 +143,4 @@ DEPENDENCIES
jazzy jazzy
BUNDLED WITH 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")

133
Makefile
View File

@ -2,16 +2,21 @@ TEMPORARY_FOLDER?=/tmp/SwiftLint.dst
PREFIX?=/usr/local PREFIX?=/usr/local
BUILD_TOOL?=xcodebuild BUILD_TOOL?=xcodebuild
XCODEFLAGS=-scheme 'swiftlint' \ XCODEFLAGS=-workspace 'SwiftLint.xcworkspace' \
-scheme 'swiftlint' \
DSTROOT=$(TEMPORARY_FOLDER) \ DSTROOT=$(TEMPORARY_FOLDER) \
OTHER_LDFLAGS=-Wl,-headerpad_max_install_names OTHER_LDFLAGS=-Wl,-headerpad_max_install_names
SWIFT_BUILD_FLAGS=--configuration release -Xlinker -dead_strip SWIFT_BUILD_FLAGS=--configuration release
UNAME=$(shell uname)
ifeq ($(UNAME), Darwin)
USE_SWIFT_STATIC_STDLIB:=$(shell test -d $$(dirname $$(xcrun --find swift))/../lib/swift_static/macosx && echo yes)
ifeq ($(USE_SWIFT_STATIC_STDLIB), yes)
SWIFT_BUILD_FLAGS+= -Xswiftc -static-stdlib
endif
endif
SWIFTLINT_EXECUTABLE_PARENT=.build/universal SWIFTLINT_EXECUTABLE=$(shell swift build $(SWIFT_BUILD_FLAGS) --show-bin-path)/swiftlint
SWIFTLINT_EXECUTABLE=$(SWIFTLINT_EXECUTABLE_PARENT)/swiftlint
ARTIFACT_BUNDLE_PATH=$(TEMPORARY_FOLDER)/SwiftLintBinary.artifactbundle
TSAN_LIB=$(subst bin/swift,lib/swift/clang/lib/darwin/libclang_rt.tsan_osx_dynamic.dylib,$(shell xcrun --find swift)) TSAN_LIB=$(subst bin/swift,lib/swift/clang/lib/darwin/libclang_rt.tsan_osx_dynamic.dylib,$(shell xcrun --find swift))
TSAN_SWIFT_BUILD_FLAGS=-Xswiftc -sanitize=thread TSAN_SWIFT_BUILD_FLAGS=-Xswiftc -sanitize=thread
@ -24,35 +29,41 @@ LICENSE_PATH="$(shell pwd)/LICENSE"
OUTPUT_PACKAGE=SwiftLint.pkg OUTPUT_PACKAGE=SwiftLint.pkg
VERSION_STRING=$(shell ./tools/get-version) SWIFTLINT_PLIST=Source/swiftlint/Supporting Files/Info.plist
SWIFTLINTFRAMEWORK_PLIST=Source/SwiftLintFramework/Supporting Files/Info.plist
.PHONY: all clean build install package test uninstall docs VERSION_STRING=$(shell /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$(SWIFTLINT_PLIST)")
.PHONY: all bootstrap clean build install package test uninstall docs
all: build all: build
sourcery: Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift Source/SwiftLintCore/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift sourcery: Source/SwiftLintFramework/Models/MasterRuleList.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift Tests/LinuxMain.swift
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil Tests/LinuxMain.swift: Tests/*/*.swift .sourcery/LinuxMain.stencil
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery sourcery --sources Tests --exclude-sources Tests/SwiftLintFrameworkTests/Resources --templates .sourcery/LinuxMain.stencil --output .sourcery --force-parse generated
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift mv .sourcery/LinuxMain.generated.swift Tests/LinuxMain.swift
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil Source/SwiftLintFramework/Models/MasterRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/MasterRuleList.stencil
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/MasterRuleList.stencil --output .sourcery
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.swift mv .sourcery/MasterRuleList.generated.swift Source/SwiftLintFramework/Models/MasterRuleList.swift
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLint*/Rules/**/*.swift .sourcery/GeneratedTests.stencil Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/AutomaticRuleTests.stencil
./tools/sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/AutomaticRuleTests.stencil --output .sourcery
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift mv .sourcery/AutomaticRuleTests.generated.swift Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift
test: clean_xcode bootstrap:
script/bootstrap
test: clean_xcode bootstrap
$(BUILD_TOOL) $(XCODEFLAGS) test $(BUILD_TOOL) $(XCODEFLAGS) test
test_tsan: test_tsan:
swift build --build-tests $(TSAN_SWIFT_BUILD_FLAGS) swift build --build-tests $(TSAN_SWIFT_BUILD_FLAGS)
DYLD_INSERT_LIBRARIES=$(TSAN_LIB) $(TSAN_XCTEST) $(TSAN_TEST_BUNDLE) DYLD_INSERT_LIBRARIES=$(TSAN_LIB) $(TSAN_XCTEST) $(TSAN_TEST_BUNDLE)
write_xcodebuild_log: write_xcodebuild_log: bootstrap
xcodebuild -scheme swiftlint clean build-for-testing -destination "platform=macOS" > xcodebuild.log xcodebuild -workspace SwiftLint.xcworkspace -scheme swiftlint clean build-for-testing > xcodebuild.log
analyze: write_xcodebuild_log analyze: write_xcodebuild_log
swift run -c release swiftlint analyze --strict --compiler-log-path xcodebuild.log swift run -c release swiftlint analyze --strict --compiler-log-path xcodebuild.log
@ -63,19 +74,14 @@ analyze_autocorrect: write_xcodebuild_log
clean: clean:
rm -f "$(OUTPUT_PACKAGE)" rm -f "$(OUTPUT_PACKAGE)"
rm -rf "$(TEMPORARY_FOLDER)" rm -rf "$(TEMPORARY_FOLDER)"
rm -f "./*.zip" "bazel.tar.gz" "bazel.tar.gz.sha256" rm -f "./portable_swiftlint.zip"
swift package clean swift package clean
clean_xcode: clean_xcode:
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean $(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
build: build:
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)" swift build $(SWIFT_BUILD_FLAGS)
bazel build --config release universal_swiftlint
$(eval SWIFTLINT_BINARY := $(shell bazel cquery --config release --output=files universal_swiftlint))
mv "$(SWIFTLINT_BINARY)" "$(SWIFTLINT_EXECUTABLE)"
chmod +w "$(SWIFTLINT_EXECUTABLE)"
strip -rSTX "$(SWIFTLINT_EXECUTABLE)"
build_with_disable_sandbox: build_with_disable_sandbox:
swift build --disable-sandbox $(SWIFT_BUILD_FLAGS) swift build --disable-sandbox $(SWIFT_BUILD_FLAGS)
@ -101,50 +107,25 @@ portable_zip: installables
cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)" cp -f "$(LICENSE_PATH)" "$(TEMPORARY_FOLDER)"
(cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip" (cd "$(TEMPORARY_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./portable_swiftlint.zip"
spm_artifactbundle_macos: installables package: 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"
chmod +x "$(TMP_FOLDER)/swiftlint"
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)"
pkgbuild \ pkgbuild \
--identifier "io.realm.swiftlint" \ --identifier "io.realm.swiftlint" \
--install-location "/usr/local/bin" \ --install-location "/" \
--root "$(PACKAGE_ROOT)" \ --root "$(TEMPORARY_FOLDER)" \
--version "$(VERSION_STRING)" \ --version "$(VERSION_STRING)" \
"$(OUTPUT_PACKAGE)" "$(OUTPUT_PACKAGE)"
bazel_release: archive:
bazel build :release carthage build --no-skip-current --platform mac
mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 . carthage archive SwiftLintFramework
docker_image: release: package archive portable_zip
docker build --platform linux/amd64 --force-rm --tag swiftlint .
docker_test: docker_test:
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.7-focal swift test --parallel docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm norionomura/swift:51 swift test --parallel
docker_htop: docker_htop:
docker run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset docker run -it --rm --pid=container:swiftlint terencewestphal/htop || reset
# https://irace.me/swift-profiling # https://irace.me/swift-profiling
display_compilation_time: display_compilation_time:
@ -152,8 +133,8 @@ display_compilation_time:
publish: publish:
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
bundle install pod trunk push SwiftLintFramework.podspec
bundle exec pod trunk push SwiftLint.podspec pod trunk push SwiftLint.podspec
docs: docs:
swift run swiftlint generate-docs swift run swiftlint generate-docs
@ -161,32 +142,22 @@ docs:
bundle exec jazzy bundle exec jazzy
get_version: get_version:
@echo "$(VERSION_STRING)" @echo $(VERSION_STRING)
release: push_version:
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),) ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
$(error git state is not clean) $(error git state is not clean)
endif endif
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS))) $(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' )) $(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
@sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md @sed -i '' 's/## Master/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintCore/Models/Version.swift @sed 's/__VERSION__/$(NEW_VERSION)/g' script/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
@sed -e '3s/.*/ version = "$(NEW_VERSION)",/' -i '' MODULE.bazel @/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(NEW_VERSION)" "$(SWIFTLINTFRAMEWORK_PLIST)"
make clean @/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(NEW_VERSION)" "$(SWIFTLINT_PLIST)"
make package
make bazel_release
make portable_zip
make spm_artifactbundle_macos
./tools/update-artifact-bundle.sh "$(NEW_VERSION)"
git commit -a -m "release $(NEW_VERSION)" git commit -a -m "release $(NEW_VERSION)"
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)" git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
git push origin HEAD git push origin master
git push origin $(NEW_VERSION) git push origin $(NEW_VERSION)
./tools/create-github-release.sh "$(NEW_VERSION)"
make publish
./tools/add-new-changelog-section.sh
git commit -a -m "Add new changelog section"
git push origin HEAD
%: %:
@: @:

View File

@ -1,77 +1,97 @@
{ {
"pins" : [ "object": {
"pins": [
{ {
"identity" : "collectionconcurrencykit", "package": "Commandant",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/Carthage/Commandant.git",
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", "state": {
"state" : { "branch": null,
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", "revision": "ab68611013dec67413628ac87c1f29e8427bc8e4",
"version" : "0.2.0" "version": "0.17.0"
} }
}, },
{ {
"identity" : "cryptoswift", "package": "CwlCatchException",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state": {
"state" : { "branch": null,
"revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f", "revision": "7cd2f8cacc4d22f21bc0b2309c3b18acf7957b66",
"version" : "1.7.2" "version": "1.2.0"
} }
}, },
{ {
"identity" : "sourcekitten", "package": "CwlPreconditionTesting",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git",
"location" : "https://github.com/jpsim/SourceKitten.git", "state": {
"state" : { "branch": null,
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56", "revision": "c228db5d2ad1b01ebc84435e823e6cca4e3db98b",
"version" : "0.34.1" "version": "1.2.0"
} }
}, },
{ {
"identity" : "swift-argument-parser", "package": "Nimble",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/Quick/Nimble.git",
"location" : "https://github.com/apple/swift-argument-parser.git", "state": {
"state" : { "branch": null,
"revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86", "revision": "b02b00b30b6353632aa4a5fb6124f8147f7140c0",
"version" : "1.2.1" "version": "8.0.5"
} }
}, },
{ {
"identity" : "swift-syntax", "package": "Quick",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/Quick/Quick.git",
"location" : "https://github.com/apple/swift-syntax.git", "state": {
"state" : { "branch": null,
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5", "revision": "33682c2f6230c60614861dfc61df267e11a1602f",
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a" "version": "2.2.0"
} }
}, },
{ {
"identity" : "swiftytexttable", "package": "SourceKitten",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/jpsim/SourceKitten.git",
"location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", "state": {
"state" : { "branch": null,
"revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", "revision": "77a4dbbb477a8110eb8765e3c44c70fb4929098f",
"version" : "0.9.0" "version": "0.29.0"
} }
}, },
{ {
"identity" : "swxmlhash", "package": "SwiftSyntax",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/apple/swift-syntax.git",
"location" : "https://github.com/drmohundro/SWXMLHash.git", "state": {
"state" : { "branch": null,
"revision" : "4d0f62f561458cbe1f732171e625f03195151b60", "revision": "3e3eb191fcdbecc6031522660c4ed6ce25282c25",
"version" : "7.0.1" "version": "0.50100.0"
} }
}, },
{ {
"identity" : "yams", "package": "SwiftyTextTable",
"kind" : "remoteSourceControl", "repositoryURL": "https://github.com/scottrhoyt/SwiftyTextTable.git",
"location" : "https://github.com/jpsim/Yams.git", "state": {
"state" : { "branch": null,
"revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a", "revision": "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3",
"version" : "5.0.5" "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": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f",
"version": "2.0.0"
} }
} }
], ]
"version" : 2 },
"version": 1
} }

View File

@ -1,121 +1,59 @@
// swift-tools-version:5.7 // swift-tools-version:5.0
import PackageDescription import PackageDescription
#if canImport(CommonCrypto)
private let addCryptoSwift = false
#else
private let addCryptoSwift = true
#endif
#if compiler(>=5.1.0)
private let addSwiftSyntax = true
#else
private let addSwiftSyntax = false
#endif
let package = Package( let package = Package(
name: "SwiftLint", name: "SwiftLint",
platforms: [.macOS(.v12)], platforms: [
.macOS(.v10_12)
],
products: [ products: [
.executable(name: "swiftlint", targets: ["swiftlint"]), .executable(name: "swiftlint", targets: ["swiftlint"]),
.library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]), .library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"])
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")), .package(url: "https://github.com/Carthage/Commandant.git", .upToNextMinor(from: "0.17.0")),
.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.29.0")),
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")), .package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0"),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"), ] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.0.0"))] : []) +
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.7.2")) (addSwiftSyntax ? [.package(url: "https://github.com/apple/swift-syntax.git", .exact("0.50100.0"))] : []),
],
targets: [ targets: [
.plugin( .target(
name: "SwiftLintPlugin",
capability: .buildTool(),
dependencies: [
.target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),
.target(name: "swiftlint", condition: .when(platforms: [.linux]))
]
),
.executableTarget(
name: "swiftlint", name: "swiftlint",
dependencies: [ dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"), "Commandant",
"CollectionConcurrencyKit",
"SwiftLintFramework", "SwiftLintFramework",
"SwiftyTextTable", "SwiftyTextTable",
] ]
), ),
.testTarget(
name: "CLITests",
dependencies: [
"swiftlint"
]
),
.target(
name: "SwiftLintCore",
dependencies: [
.product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux])),
.target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])),
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftyTextTable", package: "SwiftyTextTable"),
.product(name: "Yams", package: "Yams"),
]
),
.target(
name: "SwiftLintBuiltInRules",
dependencies: ["SwiftLintCore"]
),
.target(
name: "SwiftLintExtraRules",
dependencies: ["SwiftLintCore"]
),
.target( .target(
name: "SwiftLintFramework", name: "SwiftLintFramework",
dependencies: [ dependencies: [
"SwiftLintBuiltInRules", "SourceKittenFramework",
"SwiftLintCore", "Yams",
"SwiftLintExtraRules" ] + (addCryptoSwift ? ["CryptoSwift"] : []) +
] (addSwiftSyntax ? ["SwiftSyntax"] : [])
),
.target(name: "DyldWarningWorkaround"),
.target(
name: "SwiftLintTestHelpers",
dependencies: [
"SwiftLintFramework"
],
path: "Tests/SwiftLintTestHelpers"
), ),
.testTarget( .testTarget(
name: "SwiftLintFrameworkTests", name: "SwiftLintFrameworkTests",
dependencies: [ dependencies: [
"SwiftLintFramework", "SwiftLintFramework"
"SwiftLintTestHelpers"
], ],
exclude: [ exclude: [
"Resources", "Resources",
] ]
),
.testTarget(
name: "GeneratedTests",
dependencies: [
"SwiftLintFramework",
"SwiftLintTestHelpers"
]
),
.testTarget(
name: "IntegrationTests",
dependencies: [
"SwiftLintFramework",
"SwiftLintTestHelpers"
]
),
.testTarget(
name: "ExtraRulesTests",
dependencies: [
"SwiftLintFramework",
"SwiftLintTestHelpers"
]
),
.binaryTarget(
name: "SwiftLintBinary",
url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"
) )
] ]
) )

View File

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

View File

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

528
README.md
View File

@ -1,14 +1,15 @@
# SwiftLint # SwiftLint
A tool to enforce Swift style and conventions, loosely based on the now archived [GitHub Swift Style Guide](https://github.com/github/swift-style-guide). SwiftLint enforces the style guide rules that are generally accepted by the Swift community. These rules are well described in popular style guides like [Kodeco's Swift Style Guide](https://github.com/kodecocodes/swift-style-guide). A tool to enforce Swift style and conventions, loosely based on
[GitHub's Swift Style Guide](https://github.com/github/swift-style-guide).
SwiftLint hooks into [Clang](http://clang.llvm.org) and SwiftLint hooks into [Clang](http://clang.llvm.org) and
[SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the [SourceKit](http://www.jpsim.com/uncovering-sourcekit) to use the
[AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) representation
of your source files for more accurate results. of your source files for more accurate results.
[![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) [![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=main)](https://codecov.io/github/realm/SwiftLint?branch=main) [![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=master)](https://codecov.io/github/realm/SwiftLint?branch=master)
![](assets/screenshot.png) ![](assets/screenshot.png)
@ -16,7 +17,7 @@ This project adheres to the [Contributor Covenant Code of Conduct](https://realm
By participating, you are expected to uphold this code. Please report By participating, you are expected to uphold this code. Please report
unacceptable behavior to [info@realm.io](mailto:info@realm.io). unacceptable behavior to [info@realm.io](mailto:info@realm.io).
> Language Switch: [中文](https://github.com/realm/SwiftLint/blob/main/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/main/README_KR.md). > Language Switch: [中文](https://github.com/realm/SwiftLint/blob/master/README_CN.md), [한국어](https://github.com/realm/SwiftLint/blob/master/README_KR.md).
## Installation ## Installation
@ -41,7 +42,7 @@ in your Script Build Phases.
This is the recommended way to install a specific version of SwiftLint since it supports This is the recommended way to install a specific version of SwiftLint since it supports
installing a pinned version rather than simply the latest (which is the case with Homebrew). installing a pinned version rather than simply the latest (which is the case with Homebrew).
Note that this will add the SwiftLint binaries, its dependencies' binaries, and the Swift binary Note that this will add the SwiftLint binaries, its dependencies' binaries and the Swift binary
library distribution to the `Pods/` directory, so checking in this directory to SCM such as library distribution to the `Pods/` directory, so checking in this directory to SCM such as
git is discouraged. git is discouraged.
@ -57,77 +58,22 @@ You can also install SwiftLint by downloading `SwiftLint.pkg` from the
[latest GitHub release](https://github.com/realm/SwiftLint/releases/latest) and [latest GitHub release](https://github.com/realm/SwiftLint/releases/latest) and
running it. running it.
### Installing from source: ### Compiling from source:
You can also build and install from source by cloning this project and running You can also build from source by cloning this project and running
`make install` (Xcode 13.3 or later). `script/bootstrap; make install` (Xcode 11.0 or later).
### Using Bazel ### Known Installation Issues On MacOS Before 10.14.4
Put this in your `MODULE.bazel`: Starting with [SwiftLint 0.32.0](https://github.com/realm/SwiftLint/releases/tag/0.32.0), if you get
an error similar to `dyld: Symbol not found: _$s11SubSequenceSlTl` when running SwiftLint,
you'll need to install the [Swift 5 Runtime Support for Command Line Tools](https://support.apple.com/kb/DL1998).
```bzl Alternatively, you can:
bazel_dep(name = "swiftlint", version = "0.50.4", repo_name = "SwiftLint")
```
Or put this in your `WORKSPACE`: * Update to macOS 10.14.4 or later
* Install Xcode 10.2 or later at `/Applications/Xcode.app`
<details> * Rebuild SwiftLint from source using Xcode 10.2 or later
<summary>WORKSPACE</summary>
```bzl
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "build_bazel_rules_apple",
sha256 = "f94e6dddf74739ef5cb30f000e13a2a613f6ebfa5e63588305a71fce8a8a9911",
url = "https://github.com/bazelbuild/rules_apple/releases/download/1.1.3/rules_apple.1.1.3.tar.gz",
)
load(
"@build_bazel_rules_apple//apple:repositories.bzl",
"apple_rules_dependencies",
)
apple_rules_dependencies()
load(
"@build_bazel_rules_swift//swift:repositories.bzl",
"swift_rules_dependencies",
)
swift_rules_dependencies()
load(
"@build_bazel_rules_swift//swift:extras.bzl",
"swift_rules_extra_dependencies",
)
swift_rules_extra_dependencies()
http_archive(
name = "SwiftLint",
sha256 = "7c454ff4abeeecdd9513f6293238a6d9f803b587eb93de147f9aa1be0d8337c4",
url = "https://github.com/realm/SwiftLint/releases/download/0.49.1/bazel.tar.gz",
)
load("@SwiftLint//bazel:repos.bzl", "swiftlint_repos")
swiftlint_repos()
load("@SwiftLint//bazel:deps.bzl", "swiftlint_deps")
swiftlint_deps()
```
</details>
Then you can run SwiftLint in the current directory with this command:
```console
bazel run -c opt @SwiftLint//:swiftlint
```
## Usage ## Usage
@ -136,113 +82,49 @@ bazel run -c opt @SwiftLint//:swiftlint
To get a high-level overview of recommended ways to integrate SwiftLint into your project, To get a high-level overview of recommended ways to integrate SwiftLint into your project,
we encourage you to watch this presentation or read the transcript: we encourage you to watch this presentation or read the transcript:
[![Presentation](assets/presentation.svg)](https://youtu.be/9Z1nTMTejqU) [![Presentation](assets/presentation.svg)](https://academy.realm.io/posts/slug-jp-simard-swiftlint/)
### Xcode ### Xcode
Integrate SwiftLint into your Xcode project to get warnings and errors displayed Integrate SwiftLint into an Xcode scheme to get warnings and errors displayed
in the issue navigator. in the IDE. Just add a new "Run Script Phase" with:
To do this select the project in the file navigator, then select 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 ```bash
if [[ "$(uname -m)" == arm64 ]]; then if which swiftlint >/dev/null; then
export PATH="/opt/homebrew/bin:$PATH"
fi
if which swiftlint > /dev/null; then
swiftlint swiftlint
else else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi fi
``` ```
or you can create a symbolic link in `/usr/local/bin` pointing to the actual binary: ![](assets/runscript.png)
```bash Alternatively, if you've installed SwiftLint via CocoaPods the script should look like this:
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
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
your project for remaining violations.
If you've installed SwiftLint via CocoaPods the script should look like this:
```bash ```bash
"${PODS_ROOT}/SwiftLint/swiftlint" "${PODS_ROOT}/SwiftLint/swiftlint"
``` ```
### Plug-in Support #### Format on Save Xcode Plugin
SwiftLint can be used as a build tool plug-in for both Xcode projects as well as To run `swiftlint autocorrect` on save in Xcode, install the
Swift packages. [SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) plugin from Alcatraz.
> Due to limitations with Swift Package Manager Plug-ins this is only This plugin will not work with Xcode 8 or later without disabling SIP.
recommended for projects that have a SwiftLint configuration in their root directory as This is not recommended.
there is currently no way to pass any additional options to the SwiftLint executable.
#### Xcode ### AppCode
You can integrate SwiftLint as an Xcode Build Tool Plug-in if you're working To integrate SwiftLint with AppCode, install
with a project in Xcode. [this plugin](https://plugins.jetbrains.com/plugin/9175) and configure
SwiftLint's installed path in the plugin's preferences.
The `autocorrect` action is available via `⌥⏎`.
Add SwiftLint as a package dependency to your project without linking any of the ### Atom
products.
Select the target you want to add linting to and open the `Build Phases` inspector. To integrate SwiftLint with [Atom](https://atom.io/), install the
Open `Run Build Tool Plug-ins` and select the `+` button. [`linter-swiftlint`](https://atom.io/packages/linter-swiftlint) package from
Select `SwiftLintPlugin` from the list and add it to the project. APM.
![](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.
### fastlane ### fastlane
@ -266,65 +148,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 ### Command Line
``` ```
$ swiftlint help $ swiftlint help
OVERVIEW: A tool to enforce Swift style and conventions. Available commands:
USAGE: swiftlint <subcommand> analyze [Experimental] Run analysis rules
autocorrect Automatically correct warnings and errors
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 generate-docs Generates markdown documentation for all rules
lint (default) Print lint warnings and errors help Display general or command-specific help
reporters Display the list of reporters and their identifiers lint Print lint warnings and errors (default command)
rules Display the list of rules and their identifiers rules Display the list of rules and their identifiers
version Display the current version of SwiftLint version Display the current version of SwiftLint
See 'swiftlint help <subcommand>' for detailed help.
``` ```
Run `swiftlint` in the directory containing the Swift files to lint. Directories Run `swiftlint` in the directory containing the Swift files to lint. Directories
will be searched recursively. 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 (like the list of files modified by Xcode specified by the
[`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) Xcode
plugin, or modified files in the working tree based on `git ls-files -m`), you 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 can do so by passing the option `--use-script-input-files` and setting the
following instance variables: `SCRIPT_INPUT_FILE_COUNT` and 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 These are same environment variables set for input files to
[custom Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/). [custom Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/).
@ -360,48 +209,36 @@ You may also set the `TOOLCHAINS` environment variable to the reverse-DNS
notation that identifies a Swift toolchain version: notation that identifies a Swift toolchain version:
```shell ```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 On Linux, SourceKit is expected to be located in
`/usr/lib/libsourcekitdInProc.so` or specified by the `LINUX_SOURCEKIT_LIB_PATH` `/usr/lib/libsourcekitdInProc.so` or specified by the `LINUX_SOURCEKIT_LIB_PATH`
environment variable. environment variable.
### pre-commit ### Swift Version Support
SwiftLint can be run as a [pre-commit](https://pre-commit.com/) hook. Here's a reference of which SwiftLint version to use for a given Swift version.
Once [installed](https://pre-commit.com/#install), add this to the
`.pre-commit-config.yaml` in the root of your repository:
```yaml | Swift version | Last supported SwiftLint release |
repos: |:----------------|:---------------------------------|
- repo: https://github.com/realm/SwiftLint | Swift 1.x | SwiftLint 0.1.2 |
rev: 0.50.3 | Swift 2.x | SwiftLint 0.18.1 |
hooks: | Swift 3.x | SwiftLint 0.25.1 |
- id: swiftlint | Swift 4.0-4.1.x | SwiftLint 0.28.2 |
``` | Swift 4.2.x | SwiftLint 0.35.0 |
| Swift 5.x | Latest |
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 ## Rules
Over 200 rules are included in SwiftLint and the Swift community (that's you!) Over 75 rules are included in SwiftLint and the Swift community (that's you!)
continues to contribute more over time. continues to contribute more over time.
[Pull requests](CONTRIBUTING.md) are encouraged. [Pull requests](CONTRIBUTING.md) are encouraged.
You can find an updated list of rules and more information about them You can find an updated list of rules and more information about them
[here](https://realm.github.io/SwiftLint/rule-directory.html). [here](https://realm.github.io/SwiftLint/rule-directory.html).
You can also check [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules) You can also check [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)
directory to see their implementation. directory to see their implementation.
### Opt-In Rules ### Opt-In Rules
@ -479,33 +316,23 @@ run SwiftLint from. The following parameters can be configured:
Rule inclusion: Rule inclusion:
* `disabled_rules`: Disable rules from the default enabled set. * `disabled_rules`: Disable rules from the default enabled set.
* `opt_in_rules`: Enable rules that are not part of the default set. The * `opt_in_rules`: Enable rules not from the default set.
special `all` identifier will enable all opt in linter rules, except the ones * `whitelist_rules`: Acts as a whitelist, only the rules specified in this list
listed in `disabled_rules`. will be enabled. Can not be specified alongside `disabled_rules` or
* `only_rules`: Only the rules specified in this list will be enabled. `opt_in_rules`.
Cannot be specified alongside `disabled_rules` or `opt_in_rules`.
* `analyzer_rules`: This is an entirely separate list of rules that are only * `analyzer_rules`: This is an entirely separate list of rules that are only
run by the `analyze` command. All analyzer rules are opt-in, so this is the run by the `analyze` command. All analyzer rules are opt-in, so this is the
only configurable rule list, there are no equivalents for `disabled_rules` only configurable rule list (there is no disabled/whitelist equivalent).
`only_rules`.
```yaml ```yaml
# By default, SwiftLint uses a set of sensible default rules you can adjust: disabled_rules: # rule identifiers to exclude from running
disabled_rules: # rule identifiers turned on by default to exclude from running
- colon - colon
- comma - comma
- control_statement - control_statement
opt_in_rules: # some rules are turned off by default, so you need to opt-in opt_in_rules: # some rules are only opt-in
- empty_count # Find all the available rules by running: `swiftlint rules` - empty_count
# Find all the available rules by running:
# Alternatively, specify all rules explicitly by uncommenting this option: # swiftlint rules
# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
# - empty_parameters
# - vertical_whitespace
analyzer_rules: # Rules run by `swiftlint analyze`
- explicit_self
included: # paths to include during linting. `--path` is ignored if present. included: # paths to include during linting. `--path` is ignored if present.
- Source - Source
excluded: # paths to ignore during linting. Takes precedence over `included`. excluded: # paths to ignore during linting. Takes precedence over `included`.
@ -514,9 +341,8 @@ excluded: # paths to ignore during linting. Takes precedence over `included`.
- Source/ExcludedFolder - Source/ExcludedFolder
- Source/ExcludedFile.swift - Source/ExcludedFile.swift
- Source/*/ExcludedFile.swift # Exclude files with a wildcard - Source/*/ExcludedFile.swift # Exclude files with a wildcard
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
# If true, SwiftLint will not fail if no lintable files are found. - explicit_self
allow_zero_lintable_files: false
# configurable rules can be customized from this configuration file # configurable rules can be customized from this configuration file
# binary rules can set their severity level # binary rules can set their severity level
@ -550,29 +376,13 @@ identifier_name:
- id - id
- URL - URL
- GlobalAPIKey - GlobalAPIKey
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown)
``` ```
You can also use environment variables in your configuration file, You can also use environment variables in your configuration file,
by using `${SOME_VARIABLE}` in a string. by using `${SOME_VARIABLE}` in a string.
### Defining Custom Rules #### Defining Custom Rules
In addition to the rules that the main SwiftLint project ships with, SwiftLint
can also run two types of custom rules that you can define yourself in your own
projects:
#### 1. Swift Custom Rules
These rules are written the same way as the Swift-based rules that ship with
SwiftLint so they're fast, accurate, can leverage SwiftSyntax, can be unit
tested, and more.
Using these requires building SwiftLint with Bazel as described in
[this video](https://vimeo.com/820572803) or its associated code in
[github.com/jpsim/swiftlint-bazel-example](https://github.com/jpsim/swiftlint-bazel-example).
#### 2. Regex Custom Rules
You can define custom regex-based rules in your configuration file using the You can define custom regex-based rules in your configuration file using the
following syntax: following syntax:
@ -580,12 +390,10 @@ following syntax:
```yaml ```yaml
custom_rules: custom_rules:
pirates_beat_ninjas: # rule identifier pirates_beat_ninjas: # rule identifier
included: included: ".*\\.swift" # regex that defines paths to include during linting. optional.
- ".*\\.swift" # regex that defines paths to include during linting. optional. excluded: ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
excluded:
- ".*Test\\.swift" # regex that defines paths to exclude during linting. optional
name: "Pirates Beat Ninjas" # rule name. optional. name: "Pirates Beat Ninjas" # rule name. optional.
regex: "([nN]inja)" # matching pattern regex: "([n,N]inja)" # matching pattern
capture_group: 0 # number of regex capture group to highlight the rule violation at. optional. capture_group: 0 # number of regex capture group to highlight the rule violation at. optional.
match_kinds: # SyntaxKinds to match. optional. match_kinds: # SyntaxKinds to match. optional.
- comment - comment
@ -593,7 +401,7 @@ custom_rules:
message: "Pirates are better than ninjas." # violation message. optional. message: "Pirates are better than ninjas." # violation message. optional.
severity: error # violation severity. optional. severity: error # violation severity. optional.
no_hiding_in_strings: no_hiding_in_strings:
regex: "([nN]inja)" regex: "([n,N]inja)"
match_kinds: string match_kinds: string
``` ```
@ -605,40 +413,39 @@ You can filter the matches by providing one or more `match_kinds`, which will
reject matches that include syntax kinds that are not present in this list. Here reject matches that include syntax kinds that are not present in this list. Here
are all the possible syntax kinds: are all the possible syntax kinds:
* `argument` * argument
* `attribute.builtin` * attribute.builtin
* `attribute.id` * attribute.id
* `buildconfig.id` * buildconfig.id
* `buildconfig.keyword` * buildconfig.keyword
* `comment` * comment
* `comment.mark` * comment.mark
* `comment.url` * comment.url
* `doccomment` * doccomment
* `doccomment.field` * doccomment.field
* `identifier` * identifier
* `keyword` * keyword
* `number` * number
* `objectliteral` * objectliteral
* `parameter` * parameter
* `placeholder` * placeholder
* `string` * string
* `string_interpolation_anchor` * string_interpolation_anchor
* `typeidentifier` * typeidentifier
All syntax kinds used in a snippet of Swift code can be extracted asking If using custom rules alongside a whitelist, make sure to add `custom_rules` as an item under `whitelist_rules`.
[SourceKitten](https://github.com/jpsim/SourceKitten). For example,
`sourcekitten syntax --text "struct S {}"` delivers
* `source.lang.swift.syntaxtype.keyword` for the `struct` keyword and #### Nested Configurations
* `source.lang.swift.syntaxtype.identifier` for its name `S`
which match to `keyword` and `identifier` in the above list. SwiftLint supports nesting configuration files for more granular control over
the linting process.
If using custom rules in combination with `only_rules`, make sure to add * Include additional `.swiftlint.yml` files where necessary in your directory
`custom_rules` as an item under `only_rules`. structure.
* Each file will be linted using the configuration file that is in its
Unlike Swift custom rules, you can use official SwiftLint builds directory or at the deepest level of its parent directories. Otherwise the
(e.g. from Homebrew) to run regex custom rules. root configuration will be used.
* `included` is ignored for nested configurations.
### Auto-correct ### Auto-correct
@ -646,136 +453,27 @@ SwiftLint can automatically correct certain violations. Files on disk are
overwritten with a corrected version. overwritten with a corrected version.
Please make sure to have backups of these files before running 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 Standard linting is disabled while correcting because of the high likelihood of
violations (or their offsets) being incorrect after modifying a file while violations (or their offsets) being incorrect after modifying a file while
applying corrections. applying corrections.
### Analyze ### Analyze (experimental)
The `swiftlint analyze` command can lint Swift files using the The _experimental_ `swiftlint analyze` command can lint Swift files using the
full type-checked AST. The compiler log path containing the clean `swiftc` build full type-checked AST. The compiler log path containing the clean `swiftc` build
command invocation (incremental builds will fail) must be passed to `analyze` command invocation (incremental builds will fail) must be passed to `analyze`
via the `--compiler-log-path` flag. via the `--compiler-log-path` flag.
e.g. `--compiler-log-path /path/to/xcodebuild.log` 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) This command and related code in SwiftLint is subject to substantial changes at
2. Running `xcodebuild -workspace {WORKSPACE}.xcworkspace -scheme {SCHEME} > xcodebuild.log` any time while this feature is marked as experimental. Analyzer rules also tend
3. Running `swiftlint analyze --compiler-log-path xcodebuild.log` to be considerably slower than lint rules.
Analyzer rules tend to be considerably slower than lint rules.
## Using Multiple Configuration Files
SwiftLint offers a variety of ways to include multiple configuration files.
Multiple configuration files get merged into one single configuration that is then applied
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
in each project via a child configuration file.
Team-Wide Configuration:
```yaml
disabled_rules:
- force_cast
```
Project-Specific Configuration:
```yaml
opt_in_rules:
- force_cast
```
### Child / Parent Configs (Locally)
You can specify a `child_config` and / or a `parent_config` reference within a configuration file.
These references should be local paths relative to the folder of the configuration file they are specified in.
This even works recursively, as long as there are no cycles and no ambiguities.
**A child config is treated as a refinement and therefore has a higher priority**,
while a parent config is considered a base with lower priority in case of conflicts.
Here's an example, assuming you have the following file structure:
```
ProjectRoot
|_ .swiftlint.yml
|_ .swiftlint_refinement.yml
|_ Base
|_ .swiftlint_base.yml
```
To include both the refinement and the base file, your `.swiftlint.yml` should look like this:
```yaml
child_config: .swiftlint_refinement.yml
parent_config: Base/.swiftlint_base.yml
```
When merging parent and child configs, `included` and `excluded` configurations
are processed carefully to account for differences in the directory location
of the containing configuration files.
### Child / Parent Configs (Remote)
Just as you can provide local `child_config` / `parent_config` references, instead of
referencing local paths, you can just put urls that lead to configuration files.
In order for SwiftLint to detect these remote references, they must start with `http://` or `https://`.
The referenced remote configuration files may even recursively reference other
remote configuration files, but aren't allowed to include local references.
Using a remote reference, your `.swiftlint.yml` could look like this:
```yaml
parent_config: https://myteamserver.com/our-base-swiftlint-config.yml
```
Every time you run SwiftLint and have an Internet connection, SwiftLint tries to get a new version of
every remote configuration that is referenced. If this request times out, a cached version is
used if available. If there is no cached version available, SwiftLint fails but no worries, a cached version
should be there once SwiftLint has run successfully at least once.
If needed, the timeouts for the remote configuration fetching can be specified manually via the
configuration file(s) using the `remote_timeout` / `remote_timeout_if_cached` specifiers.
These values default to 2 / 1 second(s).
### Command Line
Instead of just providing one configuration file when running SwiftLint via the command line,
you can also pass a hierarchy, where the first configuration is treated as a parent,
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`
### Nested Configurations
In addition to a main configuration (the `.swiftlint.yml` file in the root folder),
you can put other configuration files named `.swiftlint.yml` into the directory structure
that then get merged as a child config, but only with an effect for those files
that are within the same directory as the config or in a deeper directory where
there isn't another configuration file. In other words: Nested configurations don't work
recursively there's a maximum number of one nested configuration per file
that may be applied in addition to the main configuration.
`.swiftlint.yml` files are only considered as a nested configuration if they have not been
used to build the main configuration already (e. g. by having been referenced via something
like `child_config: Folder/.swiftlint.yml`). Also, `parent_config` / `child_config`
specifications of nested configurations are getting ignored because there's no sense to that.
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.**
## License ## License

View File

@ -1,11 +1,11 @@
# SwiftLint # SwiftLint
SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [Kodeco's Swift 代码风格指南](https://github.com/kodecocodes/swift-style-guide)为基础。 SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具,基本上以 [GitHub's Swift 代码风格指南](https://github.com/github/swift-style-guide)为基础。
SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。 SwiftLint Hook 了 [Clang](http://clang.llvm.org) 和 [SourceKit](http://www.jpsim.com/uncovering-sourcekit) 从而能够使用 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 来表示源代码文件的更多精确结果。
[![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) [![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=main)](https://codecov.io/github/realm/SwiftLint?branch=main) [![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=master)](https://codecov.io/github/realm/SwiftLint?branch=master)
![](assets/screenshot.png) ![](assets/screenshot.png)
@ -45,7 +45,7 @@ $ mint install realm/SwiftLint
### 编译源代码: ### 编译源代码:
你也可以通过 Clone SwiftLint 的 Git 仓库到本地然后执行 `make install` (Xcode 12.5+) 编译源代码的方式来安装。 你也可以通过 Clone SwiftLint 的 Git 仓库到本地然后执行 `git submodule update --init --recursive; make install` (Xcode 10.2+) 编译源代码的方式来安装。
## 用法 ## 用法
@ -77,7 +77,7 @@ fi
#### 格式化保存 Xcode 插件 #### 格式化保存 Xcode 插件
在 Xcode 中保存时执行 `swiftlint autocorrect`,需要从 Alcatraz 安装 [SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) 插件。 在 XCode 中保存时执行 `swiftlint autocorrect`,需要从 Alcatraz 安装 [SwiftLintXcode](https://github.com/ypresto/SwiftLintXcode) 插件。
⚠ ️如果没有禁用 SIP 的话,这个插件在 Xcode 8 或者更新版本的 Xcode 上将不会工作。不推荐此操作。 ⚠ ️如果没有禁用 SIP 的话,这个插件在 Xcode 8 或者更新版本的 Xcode 上将不会工作。不推荐此操作。
@ -127,7 +127,7 @@ Available commands:
在包含有需要执行代码分析的 Swift 源码文件的目录下执行 `swiftlint` 命令,会对目录进行递归查找。 在包含有需要执行代码分析的 Swift 源码文件的目录下执行 `swiftlint` 命令,会对目录进行递归查找。
当使用 `lint` 或者 `autocorrect` 命令时,你可以通过添加 `--use-script-input-files` 选项并且设置以下实例变量:`SCRIPT_INPUT_FILE_COUNT` 和 当使用 `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/) 。 也有类似的用来设置输入文件的环境变量以 [自定义 Xcode script phases](http://indiestack.com/2014/12/speeding-up-custom-script-phases/) 。
@ -161,13 +161,26 @@ $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
在 Linux 上SourceKit 默认需要位于 `/usr/lib/libsourcekitdInProc.so` 或者通过 `LINUX_SOURCEKIT_LIB_PATH` 环境变量进行指定。 在 Linux 上SourceKit 默认需要位于 `/usr/lib/libsourcekitdInProc.so` 或者通过 `LINUX_SOURCEKIT_LIB_PATH` 环境变量进行指定。
### Swift Version Support
这里有一份 SwiftLint 版本和对应该 Swift 版本的对照表作为参考。
| Swift 版本 | 最后一个 SwiftLint 支持版本 |
|:----------------|:----------------------------|
| Swift 1.x | SwiftLint 0.1.2 |
| Swift 2.x | SwiftLint 0.18.1 |
| Swift 3.x | SwiftLint 0.25.1 |
| Swift 4.0-4.1.x | SwiftLint 0.28.2 |
| Swift 4.2.x | SwiftLint 0.35.0 |
| Swift 5.x | 最新的 |
## 规则 ## 规则
SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区(就是你!)会在以后有更多的贡献,我们鼓励提交 [Pull Requests](CONTRIBUTING.md)。 SwiftLint 已经包含了超过 75 条规则,并且我们希望 Swift 社区(就是你!)会在以后有更多的贡献,我们鼓励提交 [Pull Requests](CONTRIBUTING.md)。
你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。 你可以在 [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) 找到规则的更新列表和更多信息。
你也可以检视 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules) 目录来查看它们的实现。 你也可以检视 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules) 目录来查看它们的实现。
`opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。 `opt_in_rules` 默认是关闭的(即,你需要在你的配置文件中明确地打开它们)。
@ -219,7 +232,7 @@ let noWarning3 = NSNumber() as! Int
* `disabled_rules`: 关闭某些默认开启的规则。 * `disabled_rules`: 关闭某些默认开启的规则。
* `opt_in_rules`: 一些规则是可选的。 * `opt_in_rules`: 一些规则是可选的。
* `only_rules`: 不可以和 `disabled_rules` 或者 `opt_in_rules` 并列。类似一个白名单,只有在这个列表中的规则才是开启的。 * `whitelist_rules`: 不可以和 `disabled_rules` 或者 `opt_in_rules` 并列。类似一个白名单,只有在这个列表中的规则才是开启的。
```yaml ```yaml
disabled_rules: # 执行时排除掉的规则 disabled_rules: # 执行时排除掉的规则
@ -270,7 +283,7 @@ identifier_name:
- id - id
- URL - URL
- GlobalAPIKey - GlobalAPIKey
reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging) reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, junit, html, emoji)
``` ```
#### 定义自定义规则 #### 定义自定义规则
@ -281,14 +294,14 @@ reporter: "xcode" # 报告类型 (xcode, json, csv, checkstyle, codeclimate, jun
custom_rules: custom_rules:
pirates_beat_ninjas: # 规则标识符 pirates_beat_ninjas: # 规则标识符
name: "Pirates Beat Ninjas" # 规则名称,可选 name: "Pirates Beat Ninjas" # 规则名称,可选
regex: "([nN]inja)" # 匹配的模式 regex: "([n,N]inja)" # 匹配的模式
match_kinds: # 需要匹配的语法类型,可选 match_kinds: # 需要匹配的语法类型,可选
- comment - comment
- identifier - identifier
message: "Pirates are better than ninjas." # 提示信息,可选 message: "Pirates are better than ninjas." # 提示信息,可选
severity: error # 提示的级别,可选 severity: error # 提示的级别,可选
no_hiding_in_strings: no_hiding_in_strings:
regex: "([nN]inja)" regex: "([n,N]inja)"
match_kinds: string match_kinds: string
``` ```

View File

@ -1,11 +1,11 @@
# SwiftLint # SwiftLint
SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [Kodeco 스위프트 스타일 가이드](https://github.com/kodecocodes/swift-style-guide)에 대략적인 기반을 두고 있습니다. SwiftLint는 스위프트 스타일 및 컨벤션을 강제하기 위한 도구로, [GitHub 스위프트 스타일 가이드](https://github.com/github/swift-style-guide)에 대략적인 기반을 두고 있습니다.
SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다. SwiftLint는 좀 더 정확한 결과를 위해 [Clang](http://clang.llvm.org)과 [SourceKit](http://www.jpsim.com/uncovering-sourcekit)에 연결하여 소스 파일의 [AST](http://clang.llvm.org/docs/IntroductionToTheClangAST.html) 표현을 사용합니다.
[![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) [![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=main)](https://codecov.io/github/realm/SwiftLint?branch=main) [![codecov.io](https://codecov.io/github/realm/SwiftLint/coverage.svg?branch=master)](https://codecov.io/github/realm/SwiftLint?branch=master)
![](assets/screenshot.png) ![](assets/screenshot.png)
@ -27,11 +27,11 @@ Podfile에 아래 라인을 추가하기만 하면 됩니다.
pod 'SwiftLint' pod 'SwiftLint'
``` ```
이를 실행하면 다음번 `pod install` 실행 시 SwiftLint 바이너리 및 `Pods/`에 있는 디펜던시들을 다운로드하고, Script Build Phases에서 `${PODS_ROOT}/SwiftLint/swiftlint` 명령을 사용할 수 있게 됩니다. 이를 실행하면 다음번 `pod install` 실행시 SwiftLint 바이너리 및 `Pods/`에 있는 디펜던시들을 다운로드하고, Script Build Phases에서 `${PODS_ROOT}/SwiftLint/swiftlint` 명령을 사용할 수 있게 됩니다.
CocoaPods를 사용하면 최신 버전 외에도 SwiftLint의 특정 버전을 설치할 수 있기 때문에 이 방법을 권장합니다. (Homebrew는 최신 버전만 설치 가능) CocoaPods를 사용하면 최신 버전 외에도 SwiftLint의 특정 버전을 설치할 수 있기 때문에 이 방법을 권장합니다. (Homebrew는 최신 버전만 설치 가능)
이렇게 했을 때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉터리에 추가되기 때문에, git 등의 SCM에 이런 디렉터리들을 체크인하는 것은 권장하지 않습니다. 이렇게 했을때 SwiftLint 바이너리 및 그에 종속된 바이너리들과 스위프트 바이너리까지 `Pods/` 디렉토리에 추가되기 때문에, git 등의 SCM에 이런 디렉토리들을 체크인하는 것은 권장하지 않습니다.
### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우: ### [Mint](https://github.com/yonaskolb/mint)를 사용하는 경우:
``` ```
@ -44,7 +44,7 @@ $ mint install realm/SwiftLint
### 소스를 직접 컴파일하는 경우: ### 소스를 직접 컴파일하는 경우:
본 프로젝트를 클론해서 빌드할 수도 있습니다. `make install` 명령을 사용합니다. (Xcode 12.5 이후 버전) 본 프로젝트를 클론해서 빌드할 수도 있습니다. `git submodule update --init --recursive; make install` 명령을 사용합니다. (Xcode 10.2 이후 버전)
## 사용 방법 ## 사용 방법
@ -56,7 +56,7 @@ $ mint install realm/SwiftLint
### Xcode ### Xcode
SwiftLint를 Xcode 프로젝트에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. 프로젝트의 파일 내비게이터에서 타겟 앱을 선택 후 "Build Phases" 탭으로 이동합니다. + 버튼을 클릭한 후 "Run Script Phase"를 선택합니다. 그 후 아래 스크립트를 추가하기만 하면 됩니다. SwiftLint를 Xcode 스킴에 통합하여 IDE 상에 경고나 에러를 표시할 수 있습니다. "Run Script Phase"를 새로 만들고 아래 스크립트를 추가하기만 하면 됩니다.
```bash ```bash
if which swiftlint >/dev/null; then if which swiftlint >/dev/null; then
@ -68,34 +68,6 @@ fi
![](assets/runscript.png) ![](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를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다. CocoaPods를 사용해서 설치한 경우는 아래 스크립트를 대신 사용합니다.
```bash ```bash
@ -123,7 +95,7 @@ fastlane 과정에서 SwiftLint를 사용하려면 [공식적인 fastlane 액션
```ruby ```ruby
swiftlint( swiftlint(
mode: :lint, # SwiftLint 모드: :lint (디폴트) 아니면 :autocorrect mode: :lint, # SwiftLint 모드: :lint (디폴트) 아니면 :autocorrect
 executable: "Pods/SwiftLint/swiftlint", # SwiftLint 바이너리 경로 (선택 가능). CocoaPods를 사용해서 설치한 경우는 이 션이 중요합니다  executable: "Pods/SwiftLint/swiftlint", # SwiftLint 바이너리 경로 (선택 가능). CocoaPods를 사용해서 설치한 경우는 이 션이 중요합니다
 output_file: "swiftlint.result.json", # 결과 파일의 경로 (선택 가능)  output_file: "swiftlint.result.json", # 결과 파일의 경로 (선택 가능)
 reporter: "json", # 보고 유형 (선택 가능)  reporter: "json", # 보고 유형 (선택 가능)
 config_file: ".swiftlint-ci.yml",     # 설정 파일의 경로 (선택 가능)  config_file: ".swiftlint-ci.yml",     # 설정 파일의 경로 (선택 가능)
@ -144,16 +116,16 @@ Available commands:
version Display the current version of SwiftLint version Display the current version of SwiftLint
``` ```
스위프트 파일이 있는 디렉터리에서 `swiftlint`를 실행합니다. 디렉터리는 재귀적으로 탐색됩니다. 스위프트 파일이 있는 디렉토리에서 `swiftlint`를 실행합니다. 디렉토리는 재귀적으로 탐색됩니다.
`lint``autocorrect`를 사용할 때 여러 파일(예를 들면, [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) 플러그인에 의해 Xcode가 변경한 파일들 혹은 `git ls-files -m` 명령으로 인해 작업 트리에서 변경된 파일들)을 지정하려면 `--use-script-input-files` 옵션을 넘기고 다음 인스턴스 변수들을 설정하면 됩니다. `SCRIPT_INPUT_FILE_COUNT` and `lint``autocorrect`를 사용할 때 여러 파일(예를 들면, [`ExtraBuildPhase`](https://github.com/norio-nomura/ExtraBuildPhase) 플러그인에 의해 Xcode가 변경한 파일들 혹은 `git ls-files -m` 명령으로 인해 작업 트리에서 변경된 파일들)을 지정하려면 `--use-script-input-files` 옵션을 넘기고 다음 인스턴스 변수들을 설정하면 됩니다. `SCRIPT_INPUT_FILE_COUNT` and
`SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT - 1}` `SCRIPT_INPUT_FILE_0`, `SCRIPT_INPUT_FILE_1`...`SCRIPT_INPUT_FILE_{SCRIPT_INPUT_FILE_COUNT}`
이는 [Xcode의 커스텀 스크립트 단계](http://indiestack.com/2014/12/speeding-up-custom-script-phases/)에 입력 파일로 환경 변수를 지정하는 것과 동일합니다. 이는 [Xcode의 커스텀 스크립트 단계](http://indiestack.com/2014/12/speeding-up-custom-script-phases/)에 입력 파일로 환경 변수를 지정하는 것과 동일합니다.
### 스위프트 여러 버전에 대한 대응 ### 스위프트 여러 버전에 대한 대응
SwiftLint는 SourceKit에 연결되어 있으므로 스위프트 언어가 변화하더라도 이상 없이 동작할 수 있습니다. SwiftLint는 SourceKit에 연결되어 있으므로 스위프트 언어가 변화하더라도 이상없이 동작할 수 있습니다.
이는 전체 스위프트 컴파일러가 포함되지 않아도 되므로 SwiftLint가 간결하게 유지될 수 있습니다. SwiftLint는 데스크탑에 이미 설치되어 있는 공식 스위프트 컴파일러와 통신하기만 하면 됩니다. 이는 전체 스위프트 컴파일러가 포함되지 않아도 되므로 SwiftLint가 간결하게 유지될 수 있습니다. SwiftLint는 데스크탑에 이미 설치되어 있는 공식 스위프트 컴파일러와 통신하기만 하면 됩니다.
@ -171,7 +143,7 @@ SwiftLint가 어느 스위프트 툴체인을 사용할지 결정하는 순서
* `~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain` * `~/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
* `~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain` * `~/Applications/Xcode-beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain`
`sourcekitd.framework`은 위에서 선택된 경로의 `usr/lib/` 하위 디렉리에 존재해야 합니다. `sourcekitd.framework`은 위에서 선택된 경로의 `usr/lib/` 하위 디렉리에 존재해야 합니다.
`TOOLCHAINS` 환경 변수에 스위프트 툴체인 버전을 식별할 수 있는 값을 리버스 DNS 형식으로 지정할 수도 있습니다. `TOOLCHAINS` 환경 변수에 스위프트 툴체인 버전을 식별할 수 있는 값을 리버스 DNS 형식으로 지정할 수도 있습니다.
@ -183,9 +155,9 @@ $ TOOLCHAINS=com.apple.dt.toolchain.Swift_2_3 swiftlint autocorrect
## 룰 ## 룰
SwiftLint에는 200개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다. SwiftLint에는 75개가 넘는 룰들이 있고, 스위프트 커뮤니티(바로 여러분들!)는 이를 지속적으로 발전시켜 가고 있습니다. [풀 리퀘스트](CONTRIBUTING.md)는 언제나 환영입니다.
현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintBuiltInRules/Rules](Source/SwiftLintBuiltInRules/Rules)를 살펴보세요. 현재 구현된 룰 전체를 확인하려면 [Source/SwiftLintFramework/Rules](Source/SwiftLintFramework/Rules)를 살펴보세요.
`opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.) `opt_in_rules`는 기본적으로 비활성화되어 있습니다. (즉, 설정 파일에서 명시적으로 해당 룰을 활성화해야 합니다.)
@ -197,11 +169,11 @@ SwiftLint에는 200개가 넘는 룰들이 있고, 스위프트 커뮤니티(바
### 코드에서 룰 비활성화하기 ### 코드에서 룰 비활성화하기
소스 파일에서 아래 형식의 주석을 사용하면 룰을 비활성화할 수 있습니다. 소스 파일에서 아래 형식의 주석을 사용하면 룰을 비활성화 할 수 있습니다.
`// swiftlint:disable <룰1> [<룰2> <룰3>...]` `// swiftlint:disable <룰1> [<룰2> <룰3>...]`
비활성화된 룰은 해당 파일의 마지막까지 적용되거나, 활성화 주석이 나타날 때까지 적용됩니다. 비활성화 된 룰은 해당 파일의 마지막까지 적용되거나, 활성화 주석이 나타날 때까지 적용됩니다.
`// swiftlint:enable <룰1> [<룰2> <룰3>...]` `// swiftlint:enable <룰1> [<룰2> <룰3>...]`
@ -231,13 +203,13 @@ let noWarning3 = NSNumber() as! Int
### 설정 ### 설정
SwiftLint가 실행될 디렉리에 `.swiftlint.yml` 파일을 추가해서 SwiftLint를 설정할 수 있습니다. 아래 파라미터들을 설정 가능합니다. SwiftLint가 실행될 디렉리에 `.swiftlint.yml` 파일을 추가해서 SwiftLint를 설정할 수 있습니다. 아래 파라미터들을 설정 가능합니다.
룰 적용여부 설정: 룰 적용여부 설정:
* `disabled_rules`: 기본 활성화된 룰 중에 비활성화할 룰들을 지정합니다. * `disabled_rules`: 기본 활성화된 룰 중에 비활성화할 룰들을 지정합니다.
* `opt_in_rules`: 기본 룰이 아닌 룰들을 활성화합니다. * `opt_in_rules`: 기본 룰이 아닌 룰들을 활성화합니다.
* `only_rules`: 지정한 룰들만 활성화되도록 화이트리스트로 지정합니다. `disabled_rules``opt_in_rules`과는 같이 사용할 수 없습니다. * `whitelist_rules`: 지정한 룰들만 활성화되도록 화이트리스트로 지정합니다. `disabled_rules``opt_in_rules`과는 같이 사용할 수 없습니다.
```yaml ```yaml
disabled_rules: # 실행에서 제외할 룰 식별자들 disabled_rules: # 실행에서 제외할 룰 식별자들
@ -286,7 +258,7 @@ identifier_name:
- id - id
- URL - URL
- GlobalAPIKey - GlobalAPIKey
reporter: "xcode" # 보고 유형 (xcode, json, csv, codeclimate, checkstyle, junit, html, emoji, sonarqube, markdown, github-actions-logging) reporter: "xcode" # 보고 유형 (xcode, json, csv, checkstyle, junit, html, emoji, markdown)
``` ```
#### 커스텀 룰 정의 #### 커스텀 룰 정의
@ -296,16 +268,16 @@ reporter: "xcode" # 보고 유형 (xcode, json, csv, codeclimate, checkstyle, ju
```yaml ```yaml
custom_rules: custom_rules:
pirates_beat_ninjas: # 룰 식별자 pirates_beat_ninjas: # 룰 식별자
included: ".*.swift" # 린트 실행 시 포함할 경로를 정의하는 정규표현식. 선택 가능. included: ".*.swift" # 린트 실행시 포함할 경로를 정의하는 정규표현식. 선택 가능.
name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능. name: "Pirates Beat Ninjas" # 룰 이름. 선택 가능.
regex: "([nN]inja)" # 패턴 매칭 regex: "([n,N]inja)" # 패턴 매칭
match_kinds: # 매칭할 SyntaxKinds. 선택 가능. match_kinds: # 매칭할 SyntaxKinds. 선택 가능.
- comment - comment
- identifier - identifier
message: "Pirates are better than ninjas." # 위반 메시지. 선택 가능. message: "Pirates are better than ninjas." # 위반 메시지. 선택 가능.
severity: error # 위반 수준. 선택 가능. severity: error # 위반 수준. 선택 가능.
no_hiding_in_strings: no_hiding_in_strings:
regex: "([nN]inja)" regex: "([n,N]inja)"
match_kinds: string match_kinds: string
``` ```
@ -313,7 +285,7 @@ custom_rules:
![](assets/custom-rule.png) ![](assets/custom-rule.png)
하나 이상의 `match_kinds`를 사용해서 매칭된 결과를 필터링할 수 있습니다. 이 목록에 들어있지 않은 구문 유형이 포함된 결과는 매칭에서 제외됩니다. 사용 가능한 모든 구문 유형은 다음과 같습니다. 하나 이상의 `match_kinds`를 사용해서 매칭된 결과를 필터링 할 수 있습니다. 이 목록에 들어있지 않은 구문 유형이 포함된 결과는 매칭에서 제외됩니다. 사용 가능한 모든 구문 유형은 다음과 같습니다.
* argument * argument
* attribute.builtin * attribute.builtin
@ -339,13 +311,13 @@ custom_rules:
SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다. SwiftLint는 설정 파일을 중첩되게 구성해서 린트 과정을 더욱 세밀하게 제어할 수 있습니다.
* 디렉리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다. * 디렉리 구조에서 필요한 곳이면 어디든지 `.swiftlint.yml` 파일을 추가할 수 있습니다.
* 각 파일은 자신의 디렉터리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉터리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다. * 각 파일은 자신의 디렉토리 내에 있는 설정 파일을 사용하거나, 계층구조 상 가장 가까운 부모 디렉토리에 있는 설정 파일을 사용해서 린트됩니다. 별도로 설정 파일이 존재하지 않으면 루트에 있는 설정 파일이 사용됩니다.
* 중첩 구성에서 `excluded``included`는 무시됩니다. * 중첩 구성에서 `excluded``included`는 무시됩니다.
### 자동 수정 ### 자동 수정
SwiftLint는 일부 위반 사항들을 자동으로 수정할 수 있습니다. 디스크 상의 파일들은 수정된 버전으로 덮어 쓰여지게 됩니다. SwiftLint는 일부 위반 사항들을 자동으로 수정할 수 있습니다. 디스크상의 파일들은 수정된 버전으로 덮어 쓰여지게 됩니다.
`swiftlint autocorrect`를 실행하기 전에 파일들을 백업해주세요. 그렇지 않으면 중요한 데이터가 유실될 수도 있습니다. `swiftlint autocorrect`를 실행하기 전에 파일들을 백업해주세요. 그렇지 않으면 중요한 데이터가 유실될 수도 있습니다.

View File

@ -7,8 +7,16 @@ For SwiftLint contributors, follow these steps to cut a release:
* FabricSoftenerRule * FabricSoftenerRule
* Top Loading * Top Loading
* Fresh Out Of The Dryer * Fresh Out Of The Dryer
1. Make sure you have the latest stable Xcode version installed and 2. Push new version: `make push_version "0.2.0: Tumble Dry"`
`xcode-select`ed 3. Make sure you have the latest stable Xcode version installed and
1. Release new version: `make release "0.2.0: Tumble Dry"` `xcode-select`ed.
1. Wait for the Docker CI job to finish then run: `make zip_linux_release` 4. Create the pkg installer, framework zip, and portable zip: `make release`
1. Celebrate. :tada: 5. 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, and portable zip you just built
to the GitHub release binaries.
* Click "Publish release".
6. Publish to Homebrew and CocoaPods trunk: `make publish`
7. 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,29 +0,0 @@
import Foundation
/// Represents unused or missing import statements.
enum ImportUsage {
/// The import is unused. Range is for the entire import statement.
case unused(module: String, range: NSRange)
/// The file is missing an explicit import of the `module`.
case missing(module: String)
/// The range where the violation for this import usage should be reported.
var violationRange: NSRange? {
switch self {
case .unused(_, let range):
return range
case .missing:
return nil
}
}
/// The reason why this import usage is a violation.
var violationReason: String? {
switch self {
case .unused:
return nil
case .missing(let module):
return "Missing import for referenced module '\(module)'"
}
}
}

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,169 +0,0 @@
import Foundation
import SourceKittenFramework
struct RedundantTypeAnnotationRule: OptInRule, SubstitutionCorrectableRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "redundant_type_annotation",
name: "Redundant Type Annotation",
description: "Variables should not have redundant type annotation",
kind: .idiomatic,
nonTriggeringExamples: [
Example("var url = URL()"),
Example("var url: CustomStringConvertible = URL()"),
Example("@IBInspectable var color: UIColor = UIColor.white"),
Example("""
enum Direction {
case up
case down
}
var direction: Direction = .up
"""),
Example("""
enum Direction {
case up
case down
}
var direction = Direction.up
""")
],
triggeringExamples: [
Example("var url↓:URL=URL()"),
Example("var url↓:URL = URL(string: \"\")"),
Example("var url↓: URL = URL()"),
Example("let url↓: URL = URL()"),
Example("lazy var url↓: URL = URL()"),
Example("let alphanumerics↓: CharacterSet = CharacterSet.alphanumerics"),
Example("""
class ViewController: UIViewController {
func someMethod() {
let myVar: Int = Int(5)
}
}
"""),
Example("var isEnabled↓: Bool = true"),
Example("""
enum Direction {
case up
case down
}
var direction: Direction = Direction.up
""")
],
corrections: [
Example("var url↓: URL = URL()"): Example("var url = URL()"),
Example("let url↓: URL = URL()"): Example("let url = URL()"),
Example("let alphanumerics↓: CharacterSet = CharacterSet.alphanumerics"):
Example("let alphanumerics = CharacterSet.alphanumerics"),
Example("""
class ViewController: UIViewController {
func someMethod() {
let myVar: Int = Int(5)
}
}
"""):
Example("""
class ViewController: UIViewController {
func someMethod() {
let myVar = Int(5)
}
}
""")
]
)
func validate(file: SwiftLintFile) -> [StyleViolation] {
return violationRanges(in: file).map { range in
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, characterOffset: range.location)
)
}
}
func substitution(for violationRange: NSRange, in file: SwiftLintFile) -> (NSRange, String)? {
return (violationRange, "")
}
private let typeAnnotationPattern: String
private let expressionPattern: String
init() {
typeAnnotationPattern =
":\\s*" + // semicolon and any number of whitespaces
"\\w+" // type name
expressionPattern =
"(var|let)" + // var or let
"\\s+" + // at least single whitespace
"\\w+" + // variable name
"\\s*" + // possible whitespaces
typeAnnotationPattern +
"\\s*=\\s*" + // assignment operator with possible surrounding whitespaces
"\\w+" + // assignee name (type or keyword)
"[\\(\\.]?" // possible opening parenthesis or dot
}
func violationRanges(in file: SwiftLintFile) -> [NSRange] {
return file
.match(pattern: expressionPattern)
.filter {
$0.1 == [.keyword, .identifier, .typeidentifier, .identifier] ||
$0.1 == [.keyword, .identifier, .typeidentifier, .keyword]
}
.filter { !isFalsePositive(file: file, range: $0.0) }
.filter { !isIBInspectable(file: file, range: $0.0) }
.compactMap {
file.match(pattern: typeAnnotationPattern,
excludingSyntaxKinds: SyntaxKind.commentAndStringKinds, range: $0.0).first
}
}
private func isFalsePositive(file: SwiftLintFile, range: NSRange) -> Bool {
guard let typeNames = getPartsOfExpression(in: file, range: range) else { return false }
let lhs = typeNames.variableTypeName
let rhs = typeNames.assigneeName
if lhs == rhs || (lhs == "Bool" && (rhs == "true" || rhs == "false")) {
return false
} else {
return true
}
}
private func getPartsOfExpression(
in file: SwiftLintFile, range: NSRange
) -> (variableTypeName: String, assigneeName: String)? {
let substring = file.stringView.substring(with: range)
let components = substring.components(separatedBy: "=")
guard
components.count == 2,
let variableTypeName = components[0].components(separatedBy: ":").last?.trimmingCharacters(in: .whitespaces)
else {
return nil
}
let charactersToTrimFromRhs = CharacterSet(charactersIn: ".(").union(.whitespaces)
let assigneeName = components[1].trimmingCharacters(in: charactersToTrimFromRhs)
return (variableTypeName, assigneeName)
}
private func isIBInspectable(file: SwiftLintFile, range: NSRange) -> Bool {
guard
let byteRange = file.stringView.NSRangeToByteRange(start: range.location, length: range.length),
let dict = file.structureDictionary.structures(forByteOffset: byteRange.location).last,
let kind = dict.declarationKind,
SwiftDeclarationKind.variableKinds.contains(kind)
else { return false }
return dict.enclosedSwiftAttributes.contains(.ibinspectable)
}
}

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

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