Compare commits

..

No commits in common. "main" and "0.50.1" have entirely different histories.
main ... 0.50.1

653 changed files with 6783 additions and 11141 deletions

View File

@ -1 +0,0 @@
.build

View File

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

View File

@ -1 +1 @@
6.2.0
5.3.2

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

@ -11,19 +11,23 @@ steps:
- swift test --parallel -Xswiftc -DDISABLE_FOCUSED_EXAMPLES
- label: "Danger"
commands:
- echo "--- Install Bundler"
- gem install bundler
- echo "--- Bundle Install"
- bundle install
- echo "--- Build Danger"
- bazel build //tools:danger
- echo "+++ Run Danger"
- bundle exec danger --verbose
- ./bazel-bin/tools/danger --verbose
- label: "Analyze"
commands:
- echo "+++ Analyze"
- bazel test -c opt --test_output=streamed --test_timeout=1800 --spawn_strategy=local analyze
- label: "TSan Tests"
commands:
- echo "+++ Test"
- bazel test --test_output=errors --build_tests_only --features=tsan --test_timeout=1000 //Tests/...
- label: "Sourcery"
- bazel test --test_output=streamed --build_tests_only --features=tsan --test_timeout=1000 //Tests/...
- label: "TSan Runs"
commands:
- echo "+++ Run Sourcery"
- make --always-make sourcery
- echo "+++ Diff Files"
- git diff --quiet HEAD
- echo "--- Build"
- bazel build --config=release --features=tsan swiftlint
- echo "+++ Pre-cache SwiftLint Run"
- ./bazel-bin/swiftlint --progress --lenient
- echo "+++ Post-cache SwiftLint Run"
- ./bazel-bin/swiftlint --progress --lenient

View File

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

View File

@ -0,0 +1,36 @@
name: update_swift_syntax
on:
workflow_dispatch:
schedule:
# Mondays at 1pm UTC (9am EDT)
- cron: "0 13 * * 1"
jobs:
update_swift_syntax:
runs-on: ubuntu-latest
steps:
- name: Checkout source
uses: actions/checkout@v2
- name: Update SwiftSyntax
id: update-swift-syntax
run: ./tools/update-swift-syntax.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for changes
id: state
run: |
if ! git diff-index --quiet HEAD --; then
echo "dirty=true" >> $GITHUB_OUTPUT
fi
- name: Create PR
if: steps.state.outputs.dirty == 'true'
uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27
with:
title: Update SwiftSyntax
body: |
Diff: https://github.com/apple/swift-syntax/compare/${{ steps.update-swift-syntax.outputs.old_commit }}...${{ steps.update-swift-syntax.outputs.new_commit }}
commit-message: Update SwiftSyntax
delete-branch: true
branch: update-swift-syntax
branch-suffix: timestamp

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

76
BUILD
View File

@ -5,68 +5,29 @@ load(
"swift_library",
)
load(
"@rules_xcodeproj//xcodeproj:defs.bzl",
"@com_github_buildbuddy_io_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"],
),
exclude = ["Source/SwiftLintFramework/Rules/ExcludedFromBazel/ExtraRules.swift"],
) + ["@swiftlint_extra_rules//:extra_rules"],
module_name = "SwiftLintFramework",
visibility = ["//visibility:public"],
deps = [
":SwiftLintBuiltInRules",
":SwiftLintCore",
":SwiftLintExtraRules",
],
"@com_github_jpsim_sourcekitten//:SourceKittenFramework",
"@com_github_apple_swift_syntax//:optlibs",
"@sourcekitten_com_github_jpsim_yams//:Yams",
] + select({
"@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"],
"//conditions:default": [],
}),
)
swift_library(
@ -102,21 +63,6 @@ apple_universal_binary(
visibility = ["//visibility:public"],
)
filegroup(
name = "DyldWarningWorkaroundSources",
srcs = [
"Source/DyldWarningWorkaround/DyldWarningWorkaround.c",
"Source/DyldWarningWorkaround/include/objc_dupclass.h",
],
)
cc_library(
name = "DyldWarningWorkaround",
srcs = ["//:DyldWarningWorkaroundSources"],
includes = ["Source/DyldWarningWorkaround/include"],
alwayslink = True,
)
# Linting
filegroup(
@ -135,8 +81,6 @@ filegroup(
srcs = [
"BUILD",
"LICENSE",
"MODULE.bazel",
"//:DyldWarningWorkaroundSources",
"//:LintInputs",
"//Tests:BUILD",
"//bazel:release_files",

View File

@ -1,497 +1,3 @@
## Main
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* Mention a rule's identifier in the console message that is printed when the
rule's associated configuration entry contains invalid values.
[SimplyDanny](https://github.com/SimplyDanny)
* Silence `xct_specific_matcher` rule on "one argument asserts" if there are
potential types or tuples involved in the comparison as types and tuples do
not conform to `Equatable`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4990](https://github.com/realm/SwiftLint/issues/4990)
* Add `grouping` option to the `sorted_imports` rule allowing
to sort groups of imports defined by their preceding attributes
(e.g. `@testable`, `@_exported`, ...).
[hiltonc](https://github.com/hiltonc)
* Do not trigger `redundant_self_in_closure` rule when another idenfier `x` in
scope shadows the field accessed by `self.x` to avoid semantical changes.
[SimplyDanny](https://github.com/SimplyDanny)
[#5010](https://github.com/realm/SwiftLint/issues/5010)
#### Bug Fixes
* The option `validates_start_with_lowercase` can now be disabled by setting it
to `off`.
[SimplyDanny](https://github.com/SimplyDanny)
[#5036](https://github.com/realm/SwiftLint/issues/5036)
* Do not trigger `prefer_self_in_static_references` rule on `typealias`
declarations in classes.
[SimplyDanny](https://github.com/SimplyDanny)
[#5009](https://github.com/realm/SwiftLint/issues/5009)
* Do not trigger `prefer_self_in_static_references` rule on collection types in
classes, but on initializers like `[C]()` in all types.
[SimplyDanny](https://github.com/SimplyDanny)
[#5042](https://github.com/realm/SwiftLint/issues/5042)
* Fix false positives on `redundant_objc_attribute` rule for enums
and private members.
[Martin Redington](https://github.com/mildm8nnered)
[#4633](https://github.com/realm/SwiftLint/issues/4633)
* Fix autocorrect for `CGIntersectionRect` in `legacy_cggeometry_functions`
rule.
[Haoocen](https://github.com/Haoocen)
[#5023](https://github.com/realm/SwiftLint/pull/5023)
* Fix false positives on `sorted_first_last` rule when `first`/`last` have
a predicate.
[woxtu](https://github.com/woxtu)
[#3023](https://github.com/realm/SwiftLint/issues/3023)
## 0.52.2: Crisper Clearer Pleats
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* Exclude simple assignments of the form `self.x = x` from being reported by
the `redundant_self_in_closure` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4988](https://github.com/realm/SwiftLint/issues/4988)
#### Bug Fixes
* Make `unhandled_throwing_task` opt-in instead of enabled by default. The rule
is still prone to false positives at this point, so this makes enabling the
rule a conscious decision by end-users.
[JP Simard](https://github.com/jpsim)
[#4987](https://github.com/realm/SwiftLint/issues/4987)
* Fix `unhandled_throwing_task` false positives when the `Task` is returned or
where the throwing code is handled in a `Result` initializer.
[JP Simard](https://github.com/jpsim)
[#4987](https://github.com/realm/SwiftLint/issues/4987)
## 0.52.1: Crisp Clear Pleats
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* None.
#### Bug Fixes
* Let the `validates_start_with_lowercase` option in name configurations
expect a severity (warning or error). Not setting it disables the check.
Boolean values are now deprecated. A `true` value enables the check as an
error for the time being to keep the previous behavior.
[SimplyDanny](https://github.com/SimplyDanny)
[#2180](https://github.com/realm/SwiftLint/issues/2180)
* Fixed a false positive in `unhandled_throwing_task`.
[kylebshr](https://github.com/kylebshr)
[#4984](https://github.com/realm/SwiftLint/issues/4984)
* Fix Bazel release tarball for compiling on macOS.
[JP Simard](https://github.com/jpsim)
[#4985](https://github.com/realm/SwiftLint/issues/4985)
## 0.52.0: Crisp Clear Pleats
#### Breaking
* The `attributes` rule now expects attributes with arguments to be placed
on their own line above the declaration they are supposed to influence.
This applies to attributes with any kinds of arguments including single
key path arguments which were previously handled in a different way. This
behavior can be turned off by setting `attributes_with_arguments_always_on_line_above`
to `false.
[SimplyDanny](https://github.com/SimplyDanny)
[#4843](https://github.com/realm/SwiftLint/issues/4843)
* The internal module structure for SwiftLint has changed to split the
monolithic `SwiftLintFramework` into new `SwiftLintCore` for core linter
infrastructure, `SwiftLintBuiltInRules` for built-in rules and
`SwiftLintExtraRules` to add your own native rules to SwiftLint.
[JP Simard](https://github.com/jpsim)
#### Experimental
* None.
#### Enhancements
* Add new `superfluous_else` rule that triggers on `if`-statements when an
attached `else`-block can be removed, because all branches of the previous
`if`-block(s) would certainly exit the current scope already.
[SimplyDanny](https://github.com/SimplyDanny)
* Add `sorted_enum_cases` rule which warns when enum cases are not sorted.
[kimdv](https://github.com/kimdv)
* Add new `redundant_self_in_closure` rule that triggers in closures on
explicitly used `self` when it's actually not needed due to:
* Strongly captured `self` (`{ [self] in ... }`)
* Closure used in a struct declaration (`self` can always be omitted)
* Anonymous closures that are directly called (`{ ... }()`) as they are
definitly not escaping
* Weakly captured `self` with explicit unwrapping
[SimplyDanny](https://github.com/SimplyDanny)
[#59](https://github.com/realm/SwiftLint/issues/59)
* Extend `xct_specific_matcher` rule to check for boolean asserts on (un)equal
comparisons. The rule can be configured with the matchers that should trigger
rule violations. By default, all matchers trigger, but that can be limited to
just `one-argument-asserts` or `two-argument-asserts`.
[SimplyDanny](https://github.com/SimplyDanny)
[JP Simard](https://github.com/jpsim)
[#3726](https://github.com/realm/SwiftLint/issues/3726)
* Trigger `prefer_self_in_static_references` rule on more type references.
[SimplyDanny](https://github.com/SimplyDanny)
* Adds a new `reporters` command, to improve discoverability of reporters.
[Martin Redington](https://github.com/mildm8nnered)
[#4819](https://github.com/realm/SwiftLint/issues/4819)
* Adds `test_parent_classes` option to the `no_magic_numbers` rule.
Violations within test classes will now be ignored by default.
[Martin Redington](https://github.com/mildm8nnered)
[#4896](https://github.com/realm/SwiftLint/issues/4896)
* Stop enforcing calls to super from the override functions `setUp()`,
`tearDown()`, `setUpWithError()`, and `tearDownWithError()` in `XCTestCase`
subclasses.
[AndrewDMontgomery](https://github.com/andrewdmontgomery)
[#4875](https://github.com/realm/SwiftLint/pull/4875)
* Prepend `warning: ` to error messages so that they show in Xcode.
[whiteio](https://github.com/whiteio)
[#4923](https://github.com/realm/SwiftLint/issues/4923)
* The `attributes` rule received a new boolean option
`attributes_with_arguments_always_on_line_above` which is `true` by default.
Setting it to `false` ensures that attributes with arguments like
`@Persisted(primaryKey: true)` don't violate the rule if they are on the same
line with the variable declaration.
[SimplyDanny](https://github.com/SimplyDanny)
[#4843](https://github.com/realm/SwiftLint/issues/4843)
* Add new `unhandled_throwing_task` rule that triggers when a Task with an
implicit error type has unhandled trys or errors thrown inside its body.
This results in errors being silently discarded, which may be unexpected.
See this forum thread for more details: https://forums.swift.org/t/56066
[kylebshr](https://github.com/kylebshr)
#### Bug Fixes
* Fix `lower_acl_than_parent` rule rewriter by preserving leading whitespace.
[SimplyDanny](https://github.com/SimplyDanny)
[#4860](https://github.com/realm/SwiftLint/issues/4860)
* Ignore block comments in `let_var_whitespace` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4871](https://github.com/realm/SwiftLint/issues/4871)
* Fix false positives in `indentation_width` rule.
[Sven Münnich](https://github.com/svenmuennich)
* Do not trigger `reduce_boolean` on `reduce` methods with a first named
argument that is different from `into`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4894](https://github.com/realm/SwiftLint/issues/4894)
* Work around dyld warning about duplicate SwiftSyntax classes.
[keith](https://github.com/keith)
[#4782](https://github.com/realm/SwiftLint/issues/4782)
* Improve lint times of SwiftLintPlugin by moving the
`excludedPaths(fileManager:)` operation out of the linting iterations.
[andyyhope](https://github.com/andyyhope)
[#4844](https://github.com/realm/SwiftLint/issues/4844)
## 0.51.0: bzllint
#### Breaking
* Deprecate the `unused_capture_list` rule in favor of the Swift compiler
warning. At the same time, make it an opt-in rule.
[Cyberbeni](https://github.com/Cyberbeni)
[#4656](https://github.com/realm/SwiftLint/issues/4656)
* Deprecate the `inert_defer` rule in favor of the Swift compiler warning.
At the same time, make it an opt-in rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4615](https://github.com/realm/SwiftLint/issues/4615)
* Interpret strings in `excluded` option of `identifier_name`,
`type_name` and `generic_type_name` rules as regular expression. Existing
configurations should remain working without notice as long as they don't
contain characters that must be escaped in regular expression.
[Moly](https://github.com/kyounh12)
[#4655](https://github.com/realm/SwiftLint/pull/4655)
#### Experimental
* None.
#### Enhancements
* Add `duplicate_conditions` rule which warns when a condition is duplicated
in separate branches of the same branching statement (if-else, or switch).
[1in1](https://github.com/1in1)
[#4666](https://github.com/realm/SwiftLint/issues/4666)
* Add local links to rule descriptions to every rule listed
in `Rule Directory.md`.
[kattouf](https://github.com/kattouf)
* Make forceExclude work with directly specified files.
[jimmya](https://github.com/jimmya)
[#4609](https://github.com/realm/SwiftLint/issues/4609)
* Adds `all` pseudo-rule for `opt_in_rules` - enables all opt in rules
that are not listed in `disabled_rules`
[Martin Redington](https://github.com/mildm8nnered)
[#4540](https://github.com/realm/SwiftLint/issues/4540)
* Separate analyzer rules as an independent section in the rule directory of
the reference.
[Ethan Wong](https://github.com/GetToSet)
[#4664](https://github.com/realm/SwiftLint/pull/4664)
* Add rule identifier to output of Emoji reporter.
[SimplyDanny](https://github.com/SimplyDanny)
[#4707](https://github.com/realm/SwiftLint/issues/4707)
* Add new `direct_return` rule that triggers on `return` statements returning
variables that have been declared in the statement before only.
[SimplyDanny](https://github.com/SimplyDanny)
* Add `period_spacing` opt-in rule that checks periods are not followed
by 2 or more spaces in comments.
[Julioacarrettoni](https://github.com/Julioacarrettoni)
[#4624](https://github.com/realm/SwiftLint/pull/4624)
* Allow to pass a rule identifier to the `swiftlint docs` command to open its
specific documentation website, e.g. `swiftlint docs for_where`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4707](https://github.com/realm/SwiftLint/issues/4707)
* Allow new Quick APIs `aroundEach` and `justBeforeEach` for
`quick_discouraged_call`.
[David Steinacher](https://github.com/stonko1994)
[#4626](https://github.com/realm/SwiftLint/issues/4626)
* Add `relative-path` reporter to generate reports with relative file paths.
[Roya1v](https://github.com/roya1v)
[#4660](https://github.com/realm/SwiftLint/issues/4660)
* Let `number_separator` rule trigger on misplaced separators, e.g. `10_00`.
[SimplyDanny](https://github.com/SimplyDanny)
[#4637](https://github.com/realm/SwiftLint/issues/4637)
* Rewrite `multiline_arguments` rule using SwiftSyntax, ignoring trailing
closures.
[Marcelo Fabri](https://github.com/marcelofabri)
[#3399](https://github.com/realm/SwiftLint/issues/3399)
[#3605](https://github.com/realm/SwiftLint/issues/3605)
* Speed up linting by up to 6% updating to use a newer version of
`SwiftSyntax`.
[JP Simard](https://github.com/jpsim)
* Catch more valid `legacy_multiple` violations.
[JP Simard](https://github.com/jpsim)
* Catch more valid `no_magic_numbers` violations.
[JP Simard](https://github.com/jpsim)
* Add `blanket_disable_command` rule that checks whether
rules are re-enabled after being disabled.
[Martin Redington](https://github.com/mildm8nnered)
[#4731](https://github.com/realm/SwiftLint/pull/4731)
* Add `invalid_swiftlint_command` rule that validates
`// swiftlint:enable` and `disable` commands.
[Martin Redington](https://github.com/mildm8nnered)
[#4546](https://github.com/realm/SwiftLint/pull/4546)
* Improve `identifier_name` documentation.
[Martin Redington](https://github.com/mildm8nnered)
[#4767](https://github.com/realm/SwiftLint/issues/4767)
* Adds `include_multiline_strings` option to `indentation_width` rule.
[Martin Redington](https://github.com/mildm8nnered)
[#4248](https://github.com/realm/SwiftLint/issues/4248)
* Adds a new `summary` reporter, that displays the number of violations
of each rule in a text table.
[Martin Redington](https://github.com/mildm8nnered)
#### Bug Fixes
* Report violations in all `<scope>_length` rules when the error threshold is
smaller than the warning threshold.
[SimplyDanny](https://github.com/SimplyDanny)
[#4645](https://github.com/realm/SwiftLint/issues/4645)
* Consider custom attributes in `attributes` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4599](https://github.com/realm/SwiftLint/issues/4599)
* Fix whitespaces issue in auto-fix of `redundant_optional_initialization`
rule when multiple variable declaration are involved.
[SimplyDanny](https://github.com/SimplyDanny)
[#4794](https://github.com/realm/SwiftLint/issues/4794)
* Stop triggering `strict_fileprivate` rule on symbols implementing a protocol
in the same file.
[SimplyDanny](https://github.com/SimplyDanny)
[#4692](https://github.com/realm/SwiftLint/issues/4692)
* Fix false positives on `private_subject` rule when using
subjects inside functions.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4643](https://github.com/realm/SwiftLint/issues/4643)
* Fix for compiler directives masking subsequent `opening_brace`
violations.
[Martin Redington](https://github.com/mildm8nnered)
[#3712](https://github.com/realm/SwiftLint/issues/3712)
* Rewrite `explicit_type_interface` rule with SwiftSyntax fixing a
false-positive in if-case-let statements.
[SimplyDanny](https://github.com/SimplyDanny)
[#4548](https://github.com/realm/SwiftLint/issues/4548)
* Stop triggering `unused_capture_list` on captured variable that is only
referenced by a shorthand optional binding (`if let capturedVar { ... }`).
[SimplyDanny](https://github.com/SimplyDanny)
[#4804](https://github.com/realm/SwiftLint/issues/4804)
* Ensure that negative literals in initializers do not trigger
`no_magic_numbers` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4677](https://github.com/realm/SwiftLint/issues/4677)
* Fix caching of `indentation_width` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4121](https://github.com/realm/SwiftLint/issues/4121)
* Updated JUnit reporter to output error count and warning count.
[patricks](https://github.com/patricks)
[#4725](https://github.com/realm/SwiftLint/pull/4725)
* Fix correction on `lower_acl_than_parent` rule for `open` declarations.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4753](https://github.com/realm/SwiftLint/issues/4753)
* Fix `void_return` rule to support async and async throws functions.
[Mathias Schreck](https://github.com/lo1tuma)
[#4772](https://github.com/realm/SwiftLint/issues/4772)
* Fix false positives in `attributes` rule when using property wrappers
with keypath arguments.
[JP Simard](https://github.com/jpsim)
* Fix for `superfluous_disable_command` not being completely disabled
by `disable` commands.
[Martin Redington](https://github.com/mildm8nnered)
[#4788](https://github.com/realm/SwiftLint/issues/4788)
* Fixed correction for `trailing_comma` rule wrongly removing trailing
comments.
[Martin Redington](https://github.com/mildm8nnered)
[#4814](https://github.com/realm/SwiftLint/issues/4814)
## 0.50.3: Bundle of Towels
#### Breaking
* None.
#### Experimental
* None.
#### Enhancements
* The `SwiftLintPlugin` SwiftPM plugin now uses a prebuilt binary on
macOS.
[Tony Arnold](https://github.com/tonyarnold)
[JP Simard](https://github.com/jpsim)
[#4558](https://github.com/realm/SwiftLint/issues/4558)
* Don't trigger `shorthand_operator` violations inside a shorthand
operator function declaration.
[Marcelo Fabri](https://github.com/marcelofabri)
[#4611](https://github.com/realm/SwiftLint/issues/4611)
* The `balanced_xctest_lifecycle`, `single_test_class`,
`empty_xctest_method` and `test_case_accessibility` rules will now be
applied to subclasses of `QuickSpec`, as well as `XCTestCase`, by
default.
[Martin Redington](https://github.com/mildm8nnered)
* Add `test_parent_classes` option to `balanced_xctest_lifecycle`,
`single_test_class` and `empty_xctest_method` rules.
[Martin Redington](https://github.com/mildm8nnered)
[#4200](https://github.com/realm/SwiftLint/issues/4200)
* Show warnings in the console for Analyzer rules that are listed in the
`opt_in_rules` configuration section.
[SimplyDanny](https://github.com/SimplyDanny)
[#4612](https://github.com/realm/SwiftLint/issues/4612)
#### Bug Fixes
* Fix configuration parsing error in `unused_declaration` rule.
[SimplyDanny](https://github.com/SimplyDanny)
[#4612](https://github.com/realm/SwiftLint/issues/4612)
* Skip `defer` statements being last in an `#if` block if the `#if`
statement is not itself the last statement in a block.
[SimplyDanny](https://github.com/SimplyDanny)
[#4615](https://github.com/realm/SwiftLint/issues/4615)
* Fix false positives in `empty_enum_arguments` when the called
expression is an identifier or an init call.
[Steffen Matthischke](https://github.com/heeaad)
[#4597](https://github.com/realm/SwiftLint/issues/4597)
* Fix correction issue in `comma` when there was too much whitespace
following the comma.
[JP Simard](https://github.com/jpsim)
## 0.50.1: Artisanal Clothes Pegs Fixup Edition
#### Breaking

View File

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

View File

@ -16,7 +16,7 @@ has_app_changes = !modified_files.grep(/Source/).empty?
has_test_changes = !modified_files.grep(/Tests/).empty?
has_danger_changes = !modified_files.grep(/Dangerfile|tools\/oss-check|Gemfile/).empty?
has_package_changes = !modified_files.grep(/Package\.swift/).empty?
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD|MODULE\.bazel/).empty?
has_bazel_changes = !modified_files.grep(/\.bazelrc|\.bazelversion|WORKSPACE|bazel\/|BUILD/).empty?
# Add a CHANGELOG entry for app changes
if !modified_files.include?('CHANGELOG.md') && has_app_changes

View File

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

View File

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

View File

@ -30,18 +30,14 @@ VERSION_STRING=$(shell ./tools/get-version)
all: build
sourcery: Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift Source/SwiftLintCore/Models/ReportersList.swift Tests/GeneratedTests/GeneratedTests.swift
sourcery: Source/SwiftLintFramework/Models/PrimaryRuleList.swift Tests/GeneratedTests/GeneratedTests.swift
Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift: Source/SwiftLintBuiltInRules/Rules/**/*.swift .sourcery/BuiltInRules.stencil
./tools/sourcery --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/BuiltInRules.stencil --output .sourcery
mv .sourcery/BuiltInRules.generated.swift Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift
Source/SwiftLintFramework/Models/PrimaryRuleList.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/PrimaryRuleList.stencil
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/PrimaryRuleList.stencil --output .sourcery
mv .sourcery/PrimaryRuleList.generated.swift Source/SwiftLintFramework/Models/PrimaryRuleList.swift
Source/SwiftLintCore/Models/ReportersList.swift: Source/SwiftLintCore/Reporters/*.swift .sourcery/ReportersList.stencil
./tools/sourcery --sources Source/SwiftLintCore/Reporters --templates .sourcery/ReportersList.stencil --output .sourcery
mv .sourcery/ReportersList.generated.swift Source/SwiftLintCore/Models/ReportersList.swift
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLint*/Rules/**/*.swift .sourcery/GeneratedTests.stencil
./tools/sourcery --sources Source/SwiftLintCore/Rules --sources Source/SwiftLintBuiltInRules/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
Tests/GeneratedTests/GeneratedTests.swift: Source/SwiftLintFramework/Rules/**/*.swift .sourcery/GeneratedTests.stencil
sourcery --sources Source/SwiftLintFramework/Rules --templates .sourcery/GeneratedTests.stencil --output .sourcery
mv .sourcery/GeneratedTests.generated.swift Tests/GeneratedTests/GeneratedTests.swift
test: clean_xcode
@ -69,7 +65,7 @@ clean:
clean_xcode:
$(BUILD_TOOL) $(XCODEFLAGS) -configuration Test clean
build:
build: clean
mkdir -p "$(SWIFTLINT_EXECUTABLE_PARENT)"
bazel build --config release universal_swiftlint
$(eval SWIFTLINT_BINARY := $(shell bazel cquery --config release --output=files universal_swiftlint))
@ -117,11 +113,10 @@ zip_linux: docker_image
zip_linux_release:
$(eval TMP_FOLDER := $(shell mktemp -d))
docker run --platform linux/amd64 "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
docker run "ghcr.io/realm/swiftlint:$(VERSION_STRING)" cat /usr/bin/swiftlint > "$(TMP_FOLDER)/swiftlint"
chmod +x "$(TMP_FOLDER)/swiftlint"
cp -f "$(LICENSE_PATH)" "$(TMP_FOLDER)"
(cd "$(TMP_FOLDER)"; zip -yr - "swiftlint" "LICENSE") > "./swiftlint_linux.zip"
gh release upload "$(VERSION_STRING)" swiftlint_linux.zip
package: build
$(eval PACKAGE_ROOT := $(shell mktemp -d))
@ -137,11 +132,13 @@ bazel_release:
bazel build :release
mv bazel-bin/bazel.tar.gz bazel-bin/bazel.tar.gz.sha256 .
release: clean bazel_release package portable_zip spm_artifactbundle_macos zip_linux_release
docker_image:
docker build --platform linux/amd64 --force-rm --tag swiftlint .
docker_test:
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.7-focal swift test --parallel
docker run -v `pwd`:`pwd` -w `pwd` --name swiftlint --rm swift:5.6-focal swift test --parallel
docker_htop:
docker run --platform linux/amd64 -it --rm --pid=container:swiftlint terencewestphal/htop || reset
@ -152,8 +149,8 @@ display_compilation_time:
publish:
brew update && brew bump-formula-pr --tag=$(shell git describe --tags) --revision=$(shell git rev-parse HEAD) swiftlint
bundle install
bundle exec pod trunk push SwiftLint.podspec
# Workaround for https://github.com/CocoaPods/CocoaPods/issues/11185
arch -arch x86_64 pod trunk push SwiftLint.podspec
docs:
swift run swiftlint generate-docs
@ -163,30 +160,18 @@ docs:
get_version:
@echo "$(VERSION_STRING)"
release:
push_version:
ifneq ($(strip $(shell git status --untracked-files=no --porcelain 2>/dev/null)),)
$(error git state is not clean)
endif
$(eval NEW_VERSION_AND_NAME := $(filter-out $@,$(MAKECMDGOALS)))
$(eval NEW_VERSION := $(shell echo $(NEW_VERSION_AND_NAME) | sed 's/:.*//' ))
@sed -i '' 's/## Main/## $(NEW_VERSION_AND_NAME)/g' CHANGELOG.md
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintCore/Models/Version.swift
@sed -e '3s/.*/ version = "$(NEW_VERSION)",/' -i '' MODULE.bazel
make clean
make package
make bazel_release
make portable_zip
make spm_artifactbundle_macos
./tools/update-artifact-bundle.sh "$(NEW_VERSION)"
@sed 's/__VERSION__/$(NEW_VERSION)/g' tools/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift
git commit -a -m "release $(NEW_VERSION)"
git tag -a $(NEW_VERSION) -m "$(NEW_VERSION_AND_NAME)"
git push origin HEAD
git push origin $(NEW_VERSION)
./tools/create-github-release.sh "$(NEW_VERSION)"
make publish
./tools/add-new-changelog-section.sh
git commit -a -m "Add new changelog section"
git push origin HEAD
%:
@:

View File

@ -9,22 +9,13 @@
"version" : "0.2.0"
}
},
{
"identity" : "cryptoswift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
"state" : {
"revision" : "32f641cf24fc7abc1c591a2025e9f2f572648b0f",
"version" : "1.7.2"
}
},
{
"identity" : "sourcekitten",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/SourceKitten.git",
"state" : {
"revision" : "b6dc09ee51dfb0c66e042d2328c017483a1a5d56",
"version" : "0.34.1"
"revision" : "fc12c0f182c5cf80781dd933b17a82eb98bd7c61",
"version" : "0.33.1"
}
},
{
@ -32,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser.git",
"state" : {
"revision" : "4ad606ba5d7673ea60679a61ff867cc1ff8c8e86",
"version" : "1.2.1"
"revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
"version" : "1.2.0"
}
},
{
@ -41,8 +32,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "165fc6d22394c1168ff76ab5d951245971ef07e5",
"version" : "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"
"revision" : "a82041008d2c678a97407fbd0ce420d3ab047538"
}
},
{
@ -68,8 +58,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "f47ba4838c30dbd59998a4e4c87ab620ff959e8a",
"version" : "5.0.5"
"revision" : "01835dc202670b5bb90d07f3eae41867e9ed29f6",
"version" : "5.0.1"
}
}
],

View File

@ -1,6 +1,23 @@
// swift-tools-version:5.7
import PackageDescription
#if os(macOS)
private let addCryptoSwift = false
#else
private let addCryptoSwift = true
#endif
let frameworkDependencies: [Target.Dependency] = [
.product(name: "IDEUtils", package: "swift-syntax"),
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
"Yams",
]
+ (addCryptoSwift ? ["CryptoSwift"] : [])
let package = Package(
name: "SwiftLint",
platforms: [.macOS(.v12)],
@ -10,21 +27,19 @@ let package = Package(
.plugin(name: "SwiftLintPlugin", targets: ["SwiftLintPlugin"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.1")),
.package(url: "https://github.com/apple/swift-syntax.git", exact: "509.0.0-swift-DEVELOPMENT-SNAPSHOT-2023-06-05-a"),
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.34.1")),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.5"),
.package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.2.0")),
.package(url: "https://github.com/apple/swift-syntax.git", revision: "a82041008d2c678a97407fbd0ce420d3ab047538"),
.package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMinor(from: "0.33.1")),
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.1"),
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0"),
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.7.2"))
],
.package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", from: "0.2.0")
] + (addCryptoSwift ? [.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "1.6.0"))] : []),
targets: [
.plugin(
name: "SwiftLintPlugin",
capability: .buildTool(),
dependencies: [
.target(name: "SwiftLintBinary", condition: .when(platforms: [.macOS])),
.target(name: "swiftlint", condition: .when(platforms: [.linux]))
.target(name: "swiftlint")
]
),
.executableTarget(
@ -42,44 +57,15 @@ let package = Package(
"swiftlint"
]
),
.target(
name: "SwiftLintCore",
dependencies: [
.product(name: "CryptoSwift", package: "CryptoSwift", condition: .when(platforms: [.linux])),
.target(name: "DyldWarningWorkaround", condition: .when(platforms: [.macOS])),
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "SwiftIDEUtils", package: "swift-syntax"),
.product(name: "SwiftOperators", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax"),
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxBuilder", package: "swift-syntax"),
.product(name: "SwiftyTextTable", package: "SwiftyTextTable"),
.product(name: "Yams", package: "Yams"),
]
),
.target(
name: "SwiftLintBuiltInRules",
dependencies: ["SwiftLintCore"]
),
.target(
name: "SwiftLintExtraRules",
dependencies: ["SwiftLintCore"]
),
.target(
name: "SwiftLintFramework",
dependencies: [
"SwiftLintBuiltInRules",
"SwiftLintCore",
"SwiftLintExtraRules"
]
dependencies: frameworkDependencies
),
.target(name: "DyldWarningWorkaround"),
.target(
.testTarget(
name: "SwiftLintTestHelpers",
dependencies: [
"SwiftLintFramework"
],
path: "Tests/SwiftLintTestHelpers"
]
),
.testTarget(
name: "SwiftLintFrameworkTests",
@ -112,10 +98,5 @@ let package = Package(
"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 +1,16 @@
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 proposedDirectory = sequence(first: self, next: { $0.removingLastComponent() }).first { path in
let potentialConfigurationFile = path.appending(subpath: defaultConfigurationFileName)
return potentialConfigurationFile.isAccessible()
return FileManager.default.isReadableFile(atPath: potentialConfigurationFile.string)
}
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

@ -3,55 +3,45 @@ import PackagePlugin
@main
struct SwiftLintPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
func createBuildCommands(
context: PackagePlugin.PluginContext,
target: PackagePlugin.Target
) async throws -> [PackagePlugin.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 {
let inputFilePaths = sourceTarget.sourceFiles(withSuffix: "swift")
.map(\.path)
guard inputFilePaths.isEmpty == false else {
// Don't lint anything if there are no Swift source files in this target
return []
}
var arguments = [
let swiftlint = try context.tool(named: "swiftlint")
var arguments: [String] = [
"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)"
"--cache-path", "\(context.pluginWorkDirectory)"
]
// 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)"])
if let configuration = context.package.directory.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")
arguments += inputFilePaths.map(\.string)
return [
.prebuildCommand(
.buildCommand(
displayName: "SwiftLint",
executable: tool.path,
executable: swiftlint.path,
arguments: arguments,
outputFilesDirectory: outputFilesDirectory
inputFiles: inputFilePaths,
outputFiles: [context.pluginWorkDirectory]
)
]
}
@ -61,16 +51,44 @@ struct SwiftLintPlugin: BuildToolPlugin {
import XcodeProjectPlugin
extension SwiftLintPlugin: XcodeBuildToolPlugin {
func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
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")
)
guard inputFilePaths.isEmpty == false else {
// Don't lint anything if there are no Swift source files in this target
return []
}
let swiftlint = try context.tool(named: "swiftlint")
var arguments: [String] = [
"lint",
"--cache-path", "\(context.pluginWorkDirectory)"
]
// Xcode build tool plugins don't seem to run from the project source directory, so our auto-discovery of
// configuration files doesn't work. We approximate it here.
if let configuration = context.xcodeProject.directory.firstConfigurationFileInParentDirectories() {
arguments.append(contentsOf: [
"--config", "\(configuration.string)"
])
}
arguments += inputFilePaths.map(\.string)
return [
.buildCommand(
displayName: "SwiftLint",
executable: swiftlint.path,
arguments: arguments,
inputFiles: inputFilePaths,
outputFiles: [context.pluginWorkDirectory]
)
]
}
}
#endif

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,41 +0,0 @@
struct BlanketDisableCommandConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = BlanketDisableCommandRule
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
private(set) var allowedRuleIdentifiers: Set<String> = [
"file_header",
"file_length",
"file_name",
"file_name_no_space",
"single_test_class"
]
private(set) var alwaysBlanketDisableRuleIdentifiers: Set<String> = []
var consoleDescription: String {
"severity: \(severityConfiguration.consoleDescription)" +
", allowed_rules: \(allowedRuleIdentifiers.sorted())" +
", always_blanket_disable: \(alwaysBlanketDisableRuleIdentifiers.sorted())"
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
if let allowedRuleIdentifiers = configuration["allowed_rules"] as? [String] {
self.allowedRuleIdentifiers = Set(allowedRuleIdentifiers)
}
if let alwaysBlanketDisableRuleIdentifiers = configuration["always_blanket_disable"] as? [String] {
self.alwaysBlanketDisableRuleIdentifiers = Set(alwaysBlanketDisableRuleIdentifiers)
}
}
var severity: ViolationSeverity {
severityConfiguration.severity
}
}

View File

@ -1,45 +0,0 @@
struct ExplicitTypeInterfaceConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = ExplicitTypeInterfaceRule
enum VariableKind: String, CaseIterable {
case instance
case local
case `static`
case `class`
static let all = Set(allCases)
}
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
private(set) var allowedKinds = VariableKind.all
private(set) var allowRedundancy = false
var consoleDescription: String {
let excludedKinds = VariableKind.all.subtracting(allowedKinds).map(\.rawValue).sorted()
return "severity: \(severityConfiguration.consoleDescription)" +
", excluded: \(excludedKinds)" +
", allow_redundancy: \(allowRedundancy)"
}
init() {}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
for (key, value) in configuration {
switch (key, value) {
case ("severity", let severityString as String):
try severityConfiguration.apply(configuration: severityString)
case ("excluded", let excludedStrings as [String]):
allowedKinds.subtract(excludedStrings.compactMap(VariableKind.init(rawValue:)))
case ("allow_redundancy", let allowRedundancy as Bool):
self.allowRedundancy = allowRedundancy
default:
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
}
}
}

View File

@ -1,24 +0,0 @@
struct FileNameNoSpaceConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = FileNameNoSpaceRule
var consoleDescription: String {
return "(severity) \(severityConfiguration.consoleDescription), " +
"excluded: \(excluded.sorted())"
}
private(set) var severityConfiguration = SeverityConfiguration<Parent>.warning
private(set) var excluded = Set<String>()
mutating func apply(configuration: Any) throws {
guard let configurationDict = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let severity = configurationDict["severity"] {
try severityConfiguration.apply(configuration: severity)
}
if let excluded = [String].array(of: configurationDict["excluded"]) {
self.excluded = Set(excluded)
}
}
}

View File

@ -1,40 +0,0 @@
struct ImplicitlyUnwrappedOptionalConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = ImplicitlyUnwrappedOptionalRule
// swiftlint:disable:next type_name
enum ImplicitlyUnwrappedOptionalModeConfiguration: String {
case all = "all"
case allExceptIBOutlets = "all_except_iboutlets"
init(value: Any) throws {
if let string = (value as? String)?.lowercased(),
let value = Self(rawValue: string) {
self = value
} else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
}
}
private(set) var mode = ImplicitlyUnwrappedOptionalModeConfiguration.allExceptIBOutlets
private(set) var severityConfiguration = SeverityConfiguration<Parent>.warning
var consoleDescription: String {
return "severity: \(severityConfiguration.consoleDescription)" +
", mode: \(mode.rawValue)"
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let modeString = configuration["mode"] {
try mode = ImplicitlyUnwrappedOptionalModeConfiguration(value: modeString)
}
if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
}
}

View File

@ -1,108 +0,0 @@
import Foundation
struct NameConfiguration<Parent: Rule>: RuleConfiguration, Equatable {
typealias Severity = SeverityConfiguration<Parent>
typealias SeverityLevels = SeverityLevelsConfiguration<Parent>
typealias StartWithLowercaseConfiguration = ChildOptionSeverityConfiguration<Parent>
var consoleDescription: String {
"(min_length) \(minLength.shortConsoleDescription), " +
"(max_length) \(maxLength.shortConsoleDescription), " +
"excluded: \(excludedRegularExpressions.map { $0.pattern }.sorted()), " +
"allowed_symbols: \(allowedSymbols.sorted()), " +
"validates_start_with_lowercase: \(validatesStartWithLowercase.consoleDescription)"
}
private(set) var minLength: SeverityLevels
private(set) var maxLength: SeverityLevels
private(set) var excludedRegularExpressions: Set<NSRegularExpression>
private(set) var validatesStartWithLowercase: StartWithLowercaseConfiguration
private(set) var allowedSymbols: Set<String>
var minLengthThreshold: Int {
return max(minLength.warning, minLength.error ?? minLength.warning)
}
var maxLengthThreshold: Int {
return min(maxLength.warning, maxLength.error ?? maxLength.warning)
}
var allowedSymbolsAndAlphanumerics: CharacterSet {
CharacterSet(charactersIn: allowedSymbols.joined()).union(.alphanumerics)
}
init(minLengthWarning: Int,
minLengthError: Int,
maxLengthWarning: Int,
maxLengthError: Int,
excluded: [String] = [],
allowedSymbols: [String] = [],
validatesStartWithLowercase: StartWithLowercaseConfiguration = .error) {
minLength = SeverityLevels(warning: minLengthWarning, error: minLengthError)
maxLength = SeverityLevels(warning: maxLengthWarning, error: maxLengthError)
self.excludedRegularExpressions = Set(excluded.compactMap {
try? NSRegularExpression.cached(pattern: "^\($0)$")
})
self.allowedSymbols = Set(allowedSymbols)
self.validatesStartWithLowercase = validatesStartWithLowercase
}
mutating func apply(configuration: Any) throws {
guard let configurationDict = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let minLengthConfiguration = configurationDict["min_length"] {
try minLength.apply(configuration: minLengthConfiguration)
}
if let maxLengthConfiguration = configurationDict["max_length"] {
try maxLength.apply(configuration: maxLengthConfiguration)
}
if let excluded = [String].array(of: configurationDict["excluded"]) {
self.excludedRegularExpressions = Set(excluded.compactMap {
try? NSRegularExpression.cached(pattern: "^\($0)$")
})
}
if let allowedSymbols = [String].array(of: configurationDict["allowed_symbols"]) {
self.allowedSymbols = Set(allowedSymbols)
}
if let validatesStartWithLowercase = configurationDict["validates_start_with_lowercase"] as? String {
try self.validatesStartWithLowercase.apply(configuration: validatesStartWithLowercase)
} else if let validatesStartWithLowercase = configurationDict["validates_start_with_lowercase"] as? Bool {
// TODO: [05/10/2025] Remove deprecation warning after ~2 years.
self.validatesStartWithLowercase = validatesStartWithLowercase ? .error : .off
Issue.genericWarning(
"""
The \"validates_start_with_lowercase\" configuration now expects a severity (warning or \
error). The boolean value 'true' will still enable it as an error. It is now deprecated and will be \
removed in a future release.
"""
).print()
}
}
}
extension NameConfiguration {
func severity(forLength length: Int) -> ViolationSeverity? {
if let minError = minLength.error, length < minError {
return .error
} else if let maxError = maxLength.error, length > maxError {
return .error
} else if length < minLength.warning ||
length > maxLength.warning {
return .warning
}
return nil
}
}
// MARK: - `exclude` option extensions
extension NameConfiguration {
func shouldExclude(name: String) -> Bool {
excludedRegularExpressions.contains {
!$0.matches(in: name, options: [], range: NSRange(name.startIndex..., in: name)).isEmpty
}
}
}

View File

@ -1,22 +0,0 @@
struct PrefixedTopLevelConstantConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = PrefixedTopLevelConstantRule
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
private(set) var onlyPrivateMembers = false
var consoleDescription: String {
return "severity: \(severityConfiguration.consoleDescription)" + ", only_private: \(onlyPrivateMembers)"
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
onlyPrivateMembers = (configuration["only_private"] as? Bool == true)
if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
}
}

View File

@ -1,37 +0,0 @@
struct SortedImportsConfiguration: RuleConfiguration, Equatable {
typealias Parent = SortedImportsRule
enum SortedImportsGroupingConfiguration: String {
/// Sorts import lines based on any import attributes (e.g. `@testable`, `@_exported`, etc.), followed by a case
/// insensitive comparison of the imported module name.
case attributes
/// Sorts import lines based on a case insensitive comparison of the imported module name.
case names
}
private(set) var severity = SeverityConfiguration<Parent>(.warning)
private(set) var grouping = SortedImportsGroupingConfiguration.names
var consoleDescription: String {
return "severity: \(severity.consoleDescription)"
+ ", grouping: \(grouping)"
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let rawGrouping = configuration["grouping"] {
guard let rawGrouping = rawGrouping as? String,
let grouping = SortedImportsGroupingConfiguration(rawValue: rawGrouping) else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
self.grouping = grouping
}
if let severityString = configuration["severity"] as? String {
try severity.apply(configuration: severityString)
}
}
}

View File

@ -1,37 +0,0 @@
struct StatementPositionConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = StatementPositionRule
enum StatementModeConfiguration: String {
case `default` = "default"
case uncuddledElse = "uncuddled_else"
init(value: Any) throws {
if let string = (value as? String)?.lowercased(),
let value = Self(rawValue: string) {
self = value
} else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
}
}
var consoleDescription: String {
return "(statement_mode) \(statementMode.rawValue), " +
"(severity) \(severityConfiguration.consoleDescription)"
}
private(set) var statementMode = StatementModeConfiguration.default
private(set) var severityConfiguration = SeverityConfiguration<Parent>.warning
mutating func apply(configuration: Any) throws {
guard let configurationDict = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let statementModeConfiguration = configurationDict["statement_mode"] {
try statementMode = StatementModeConfiguration(value: statementModeConfiguration)
}
if let severity = configurationDict["severity"] {
try severityConfiguration.apply(configuration: severity)
}
}
}

View File

@ -1,32 +0,0 @@
typealias BalancedXCTestLifecycleConfiguration = UnitTestConfiguration<BalancedXCTestLifecycleRule>
typealias EmptyXCTestMethodConfiguration = UnitTestConfiguration<EmptyXCTestMethodRule>
typealias SingleTestClassConfiguration = UnitTestConfiguration<SingleTestClassRule>
typealias NoMagicNumbersConfiguration = UnitTestConfiguration<NoMagicNumbersRule>
struct UnitTestConfiguration<Parent: Rule>: SeverityBasedRuleConfiguration, Equatable {
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
private(set) var testParentClasses: Set<String> = ["QuickSpec", "XCTestCase"]
var consoleDescription: String {
return "severity: \(severityConfiguration.consoleDescription)" +
", test_parent_classes: \(testParentClasses.sorted())"
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let severityString = configuration["severity"] as? String {
try severityConfiguration.apply(configuration: severityString)
}
if let extraTestParentClasses = configuration["test_parent_classes"] as? [String] {
self.testParentClasses.formUnion(extraTestParentClasses)
}
}
var severity: ViolationSeverity {
return severityConfiguration.severity
}
}

View File

@ -1,38 +0,0 @@
// swiftlint:disable:next type_name
struct VerticalWhitespaceClosingBracesConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = VerticalWhitespaceClosingBracesRule
private enum ConfigurationKey: String {
case severity = "severity"
case onlyEnforceBeforeTrivialLines = "only_enforce_before_trivial_lines"
}
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
private(set) var onlyEnforceBeforeTrivialLines = false
var consoleDescription: String {
return "severity: \(severityConfiguration.consoleDescription)" +
", \(ConfigurationKey.onlyEnforceBeforeTrivialLines.rawValue): \(onlyEnforceBeforeTrivialLines)"
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
for (string, value) in configuration {
guard let key = ConfigurationKey(rawValue: string) else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
switch (key, value) {
case (.severity, let stringValue as String):
try severityConfiguration.apply(configuration: stringValue)
case (.onlyEnforceBeforeTrivialLines, let boolValue as Bool):
onlyEnforceBeforeTrivialLines = boolValue
default:
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
}
}
}

View File

@ -1,37 +0,0 @@
struct XCTSpecificMatcherConfiguration: SeverityBasedRuleConfiguration, Equatable {
typealias Parent = XCTSpecificMatcherRule
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
private(set) var matchers = Set(Matcher.allCases)
enum Matcher: String, Hashable, CaseIterable {
case oneArgumentAsserts = "one-argument-asserts"
case twoArgumentAsserts = "two-argument-asserts"
}
private enum ConfigurationKey: String {
case severity
case matchers
}
var consoleDescription: String {
return [
"severity: \(severityConfiguration.consoleDescription)",
"\(ConfigurationKey.matchers): \(matchers.map(\.rawValue).sorted().joined(separator: ", "))"
].joined(separator: ", ")
}
mutating func apply(configuration: Any) throws {
guard let configuration = configuration as? [String: Any] else {
throw Issue.unknownConfiguration(ruleID: Parent.identifier)
}
if let severityString = configuration[ConfigurationKey.severity.rawValue] as? String {
try severityConfiguration.apply(configuration: severityString)
}
if let matchers = configuration[ConfigurationKey.matchers.rawValue] as? [String] {
self.matchers = Set(matchers.compactMap(Matcher.init(rawValue:)))
}
}
}

View File

@ -1,263 +0,0 @@
import SwiftSyntax
struct DirectReturnRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "direct_return",
name: "Direct Return",
description: "Directly return the expression instead of assigning it to a variable first",
kind: .style,
nonTriggeringExamples: [
Example("""
func f() -> Int {
let b = 2
let a = 1
return b
}
"""),
Example("""
struct S {
var a: Int {
var b = 1
b = 2
return b
}
}
"""),
Example("""
func f() -> Int {
let b = 2
f()
return b
}
"""),
Example("""
func f() -> Int {
{ i in
let b = 2
return i
}(1)
}
""")
],
triggeringExamples: [
Example("""
func f() -> Int {
let b = 2
return b
}
"""),
Example("""
struct S {
var a: Int {
var b = 1
// comment
return b
}
}
"""),
Example("""
func f() -> Bool {
let a = 1, b = true
return b
}
"""),
Example("""
func f() -> Int {
{ _ in
let b = 2
return b
}(1)
}
"""),
Example("""
func f(i: Int) -> Int {
if i > 1 {
let a = 2
return a
} else {
let b = 2, a = 1
return b
}
}
""")
],
corrections: [
Example("""
func f() -> Int {
let b = 2
return b
}
"""): Example("""
func f() -> Int {
return 2
}
"""),
Example("""
struct S {
var a: Int {
var b = 2 > 1
? f()
: 1_000
// comment
return b
}
func f() -> Int { 1 }
}
"""): Example("""
struct S {
var a: Int {
// comment
return 2 > 1
? f()
: 1_000
}
func f() -> Int { 1 }
}
"""),
Example("""
func f() -> Bool {
let a = 1, b = true
return b
}
"""): Example("""
func f() -> Bool {
let a = 1
return true
}
"""),
Example("""
func f() -> Int {
{ _ in
// A comment
let b = 2
// Another comment
return b
}(1)
}
"""): Example("""
func f() -> Int {
{ _ in
// A comment
// Another comment
return 2
}(1)
}
"""),
Example("""
func f() -> Bool {
let b : Bool = true
return b
}
"""): Example("""
func f() -> Bool {
return true as Bool
}
""")
]
)
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 var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
override func visitPost(_ statements: CodeBlockItemListSyntax) {
if let (binding, _) = statements.violation {
violations.append(binding.positionAfterSkippingLeadingTrivia)
}
}
}
private extension CodeBlockItemListSyntax {
var violation: (PatternBindingSyntax, ReturnStmtSyntax)? {
guard count >= 2, let last = last?.item,
let returnStmt = last.as(ReturnStmtSyntax.self),
let identifier = returnStmt.expression?.as(IdentifierExprSyntax.self)?.identifier.text,
let varDecl = dropLast().last?.item.as(VariableDeclSyntax.self) else {
return nil
}
let binding = varDecl.bindings.first {
$0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == identifier
}
if let binding {
return (binding, returnStmt)
}
return nil
}
}
private 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(_ statements: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
guard let (binding, returnStmt) = statements.violation,
!binding.isContainedIn(regions: disabledRegions, locationConverter: locationConverter),
let bindingList = binding.parent?.as(PatternBindingListSyntax.self),
let varDecl = bindingList.parent?.as(VariableDeclSyntax.self),
var initExpression = binding.initializer?.value else {
return super.visit(statements)
}
correctionPositions.append(binding.positionAfterSkippingLeadingTrivia)
var newStmtList = Array(statements.dropLast(2))
let newBindingList = bindingList
.filter { $0 != binding }
.enumerated()
.map { index, item in
if index == bindingList.count - 2 {
return item.with(\.trailingComma, nil)
}
return item
}
if let type = binding.typeAnnotation?.type {
initExpression = ExprSyntax(
fromProtocol: AsExprSyntax(
expression: initExpression.trimmed,
asTok: .keyword(.as).with(\.leadingTrivia, .space).with(\.trailingTrivia, .space),
typeName: type.trimmed
)
)
}
if newBindingList.isNotEmpty {
newStmtList.append(CodeBlockItemSyntax(
item: .decl(DeclSyntax(varDecl.with(\.bindings, PatternBindingListSyntax(newBindingList))))
))
newStmtList.append(CodeBlockItemSyntax(
item: .stmt(StmtSyntax(returnStmt.with(\.expression, initExpression)))
))
} else {
let leadingTrivia = varDecl.leadingTrivia.withoutTrailingIndentation +
varDecl.trailingTrivia +
returnStmt.leadingTrivia.withFirstEmptyLineRemoved
newStmtList.append(
CodeBlockItemSyntax(
item: .stmt(
StmtSyntax(
returnStmt
.with(\.expression, initExpression)
.with(\.leadingTrivia, leadingTrivia)
)
)
)
)
}
return super.visit(CodeBlockItemListSyntax(newStmtList))
}
}

View File

@ -1,128 +0,0 @@
import SwiftSyntax
struct MultilineArgumentsRule: SwiftSyntaxRule, OptInRule, ConfigurationProviderRule {
var configuration = MultilineArgumentsConfiguration()
static let description = RuleDescription(
identifier: "multiline_arguments",
name: "Multiline Arguments",
description: "Arguments should be either on the same line, or one per line",
kind: .style,
nonTriggeringExamples: MultilineArgumentsRuleExamples.nonTriggeringExamples,
triggeringExamples: MultilineArgumentsRuleExamples.triggeringExamples
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(
onlyEnforceAfterFirstClosureOnFirstLine: configuration.onlyEnforceAfterFirstClosureOnFirstLine,
firstArgumentLocation: configuration.firstArgumentLocation,
locationConverter: file.locationConverter
)
}
}
private extension MultilineArgumentsRule {
final class Visitor: ViolationsSyntaxVisitor {
let onlyEnforceAfterFirstClosureOnFirstLine: Bool
let firstArgumentLocation: MultilineArgumentsConfiguration.FirstArgumentLocation
let locationConverter: SourceLocationConverter
init(onlyEnforceAfterFirstClosureOnFirstLine: Bool,
firstArgumentLocation: MultilineArgumentsConfiguration.FirstArgumentLocation,
locationConverter: SourceLocationConverter) {
self.onlyEnforceAfterFirstClosureOnFirstLine = onlyEnforceAfterFirstClosureOnFirstLine
self.firstArgumentLocation = firstArgumentLocation
self.locationConverter = locationConverter
super.init(viewMode: .sourceAccurate)
}
override func visitPost(_ node: FunctionCallExprSyntax) {
guard node.argumentList.count > 1 else {
return
}
let functionCallPosition = node.calledExpression.positionAfterSkippingLeadingTrivia
let functionCallLine = locationConverter.location(for: functionCallPosition).line
let wrappedArguments: [Argument] = node.argumentList
.enumerated()
.compactMap { idx, argument in
Argument(element: argument, locationConverter: locationConverter, index: idx)
}
var violatingArguments = findViolations(in: wrappedArguments, functionCallLine: functionCallLine)
if onlyEnforceAfterFirstClosureOnFirstLine {
violatingArguments = removeViolationsBeforeFirstClosure(arguments: wrappedArguments,
violations: violatingArguments)
}
violations.append(contentsOf: violatingArguments.map(\.offset))
}
// MARK: - Violation Logic
private func findViolations(in arguments: [Argument],
functionCallLine: Int) -> [Argument] {
var visitedLines = Set<Int>()
if firstArgumentLocation == .sameLine {
visitedLines.insert(functionCallLine)
}
let violations = arguments.compactMap { argument -> Argument? in
let (line, idx) = (argument.line, argument.index)
let (firstVisit, _) = visitedLines.insert(line)
if idx == 0 {
switch firstArgumentLocation {
case .anyLine: return nil
case .nextLine: return line > functionCallLine ? nil : argument
case .sameLine: return line > functionCallLine ? argument : nil
}
} else {
return firstVisit ? nil : argument
}
}
// only report violations if multiline
return visitedLines.count > 1 ? violations : []
}
private func removeViolationsBeforeFirstClosure(arguments: [Argument],
violations: [Argument]) -> [Argument] {
guard let firstClosure = arguments.first(where: { $0.isClosure }),
let firstArgument = arguments.first else {
return violations
}
let violationSlice: ArraySlice<Argument> = violations
.drop { argument in
// drop violations if they precede the first closure,
// if that closure is in the first line
firstArgument.line == firstClosure.line &&
argument.line == firstClosure.line &&
argument.index <= firstClosure.index
}
return Array(violationSlice)
}
}
}
private struct Argument {
let offset: AbsolutePosition
let line: Int
let index: Int
let expression: ExprSyntax
init?(element: TupleExprElementSyntax, locationConverter: SourceLocationConverter, index: Int) {
self.offset = element.positionAfterSkippingLeadingTrivia
self.line = locationConverter.location(for: offset).line
self.index = index
self.expression = element.expression
}
var isClosure: Bool {
expression.is(ClosureExprSyntax.self)
}
}

View File

@ -1,237 +0,0 @@
import SwiftSyntax
struct PreferSelfInStaticReferencesRule: SwiftSyntaxRule, CorrectableRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "prefer_self_in_static_references",
name: "Prefer Self in Static References",
description: "Use `Self` to refer to the surrounding type name",
kind: .style,
nonTriggeringExamples: PreferSelfInStaticReferencesRuleExamples.nonTriggeringExamples,
triggeringExamples: PreferSelfInStaticReferencesRuleExamples.triggeringExamples,
corrections: PreferSelfInStaticReferencesRuleExamples.corrections
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
func correct(file: SwiftLintFile) -> [Correction] {
let ranges = Visitor(viewMode: .sourceAccurate)
.walk(file: file, handler: \.corrections)
.compactMap { file.stringView.NSRange(start: $0.start, end: $0.end) }
.filter { file.ruleEnabled(violatingRange: $0, for: self) != nil }
.reversed()
var corrections = [Correction]()
var contents = file.contents
for range in ranges {
let contentsNSString = contents.bridge()
contents = contentsNSString.replacingCharacters(in: range, with: "Self")
let location = Location(file: file, characterOffset: range.location)
corrections.append(Correction(ruleDescription: Self.description, location: location))
}
file.write(contents)
return corrections
}
}
private class Visitor: ViolationsSyntaxVisitor {
private enum ParentDeclBehavior {
case likeClass(name: String)
case likeStruct(String)
case skipReferences
var parentName: String? {
switch self {
case let .likeClass(name): return name
case let .likeStruct(name): return name
case .skipReferences: return nil
}
}
}
private enum VariableDeclBehavior {
case handleReferences
case skipReferences
}
private var parentDeclScopes = Stack<ParentDeclBehavior>()
private var variableDeclScopes = Stack<VariableDeclBehavior>()
private(set) var corrections = [(start: AbsolutePosition, end: AbsolutePosition)]()
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
parentDeclScopes.push(.likeClass(name: node.identifier.text))
return .skipChildren
}
override func visitPost(_ node: ActorDeclSyntax) {
parentDeclScopes.pop()
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
parentDeclScopes.push(.likeClass(name: node.identifier.text))
return .visitChildren
}
override func visitPost(_ node: ClassDeclSyntax) {
parentDeclScopes.pop()
}
override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind {
variableDeclScopes.push(.handleReferences)
return .visitChildren
}
override func visitPost(_ node: CodeBlockSyntax) {
variableDeclScopes.pop()
}
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
parentDeclScopes.push(.likeStruct(node.identifier.text))
return .visitChildren
}
override func visitPost(_ node: EnumDeclSyntax) {
parentDeclScopes.pop()
}
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
parentDeclScopes.push(.skipReferences)
return .visitChildren
}
override func visitPost(_ node: ExtensionDeclSyntax) {
parentDeclScopes.pop()
}
override func visit(_ node: MemberAccessExprSyntax) -> SyntaxVisitorContinueKind {
if case .likeClass = parentDeclScopes.peek() {
if node.name.tokenKind == .keyword(.self) {
return .skipChildren
}
}
return .visitChildren
}
override func visitPost(_ node: IdentifierExprSyntax) {
guard let parent = node.parent, !parent.is(SpecializeExprSyntax.self) else {
return
}
if parent.is(FunctionCallExprSyntax.self), case .likeClass = parentDeclScopes.peek() {
return
}
addViolation(on: node.identifier)
}
override func visit(_ node: MemberDeclBlockSyntax) -> SyntaxVisitorContinueKind {
if case .likeClass = parentDeclScopes.peek() {
variableDeclScopes.push(.skipReferences)
} else {
variableDeclScopes.push(.handleReferences)
}
return .visitChildren
}
override func visitPost(_ node: MemberDeclBlockSyntax) {
variableDeclScopes.pop()
}
override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
if case .likeClass = parentDeclScopes.peek(), case .identifier("selector") = node.macro.tokenKind {
return .visitChildren
}
return .skipChildren
}
override func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
if case .likeStruct = parentDeclScopes.peek() {
return .visitChildren
}
return .skipChildren
}
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
parentDeclScopes.push(.skipReferences)
return .skipChildren
}
override func visitPost(_ node: ProtocolDeclSyntax) {
parentDeclScopes.pop()
}
override func visit(_ node: ReturnClauseSyntax) -> SyntaxVisitorContinueKind {
if case .likeStruct = parentDeclScopes.peek() {
return .visitChildren
}
return .skipChildren
}
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
parentDeclScopes.push(.likeStruct(node.identifier.text))
return .visitChildren
}
override func visitPost(_ node: StructDeclSyntax) {
parentDeclScopes.pop()
}
override func visitPost(_ node: SimpleTypeIdentifierSyntax) {
guard let parent = node.parent else {
return
}
if case .likeClass = parentDeclScopes.peek(), parent.is(GenericArgumentSyntax.self) {
// Type is a generic parameter in a class.
return
}
if node.genericArguments == nil {
// Type is specialized.
addViolation(on: node.name)
}
}
override func visit(_ node: TypealiasDeclSyntax) -> SyntaxVisitorContinueKind {
if case .likeClass = parentDeclScopes.peek() {
return .skipChildren
}
return .visitChildren
}
override func visit(_ node: TypeAnnotationSyntax) -> SyntaxVisitorContinueKind {
guard case .likeStruct = parentDeclScopes.peek() else {
return .skipChildren
}
if let varDecl = node.parent?.parent?.parent?.as(VariableDeclSyntax.self) {
if varDecl.parent?.is(CodeBlockItemSyntax.self) == true // Local variable declaration
|| varDecl.bindings.onlyElement?.accessor != nil // Computed property
|| !node.type.is(SimpleTypeIdentifierSyntax.self) // Complex or collection type
{
return .visitChildren
}
}
return .skipChildren
}
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
if node.bindings.onlyElement?.accessor != nil {
// Variable declaration is a computed property.
return .visitChildren
}
if case .handleReferences = variableDeclScopes.peek() {
return .visitChildren
}
return .skipChildren
}
private func addViolation(on node: TokenSyntax) {
if let parentName = parentDeclScopes.peek()?.parentName, node.tokenKind == .identifier(parentName) {
violations.append(node.positionAfterSkippingLeadingTrivia)
corrections.append(
(start: node.positionAfterSkippingLeadingTrivia, end: node.endPositionBeforeTrailingTrivia)
)
}
}
}

View File

@ -1,232 +0,0 @@
enum PreferSelfInStaticReferencesRuleExamples {
static let nonTriggeringExamples = [
Example("""
class C {
static let primes = [2, 3, 5, 7]
func isPrime(i: Int) -> Bool { Self.primes.contains(i) }
"""),
Example("""
struct T {
static let i = 0
}
struct S {
static let i = 0
}
extension T {
static let j = S.i + T.i
static let k = { T.j }()
}
"""),
Example("""
class `Self` {
static let i = 0
func f() -> Int { Self.i }
}
"""),
Example("""
class C {
static private(set) var i = 0, j = C.i
static let k = { C.i }()
let h = C.i
@GreaterThan(C.j) var k: Int
}
""", excludeFromDocumentation: true),
Example("""
struct S {
struct T {
struct R {
static let i = 3
}
}
struct R {
static let j = S.T.R.i
}
static let j = Self.T.R.i + Self.R.j
let h = Self.T.R.i + Self.R.j
}
""", excludeFromDocumentation: true),
Example("""
class C {
static let s = 2
func f(i: Int = C.s) -> Int {
func g(@GreaterEqualThan(C.s) j: Int = C.s) -> Int { j }
return i + Self.s
}
func g() -> Any { C.self }
}
""", excludeFromDocumentation: true),
Example("""
struct Record<T> {
static func get() -> Record<T> { Record<T>() }
}
""", excludeFromDocumentation: true),
Example("""
@objc class C: NSObject {
@objc var s = ""
@objc func f() { _ = #keyPath(C.s) }
}
""", excludeFromDocumentation: true),
Example("""
class C<T> {
let i = 1
let c: C = C()
func f(c: C) -> KeyPath<C, Int> { \\Self.i }
}
""", excludeFromDocumentation: true)
]
static let triggeringExamples = [
Example("""
final class CheckCellView: NSTableCellView {
@IBOutlet var checkButton: NSButton!
override func awakeFromNib() {
checkButton.action = #selector(CheckCellView.check(_:))
}
@objc func check(_ button: AnyObject?) {}
}
"""),
Example("""
class C {
struct S {
static let i = 2
let h = S.i
}
static let i = 1
let h = C.i
var j: Int { C.i }
func f() -> Int { C.i + h }
}
"""),
Example("""
class C {
func f() {
_ = [C]()
_ = [Int: C]()
}
}
"""),
Example("""
struct S {
let j: Int
static let i = 1
static func f() -> Int { S.i }
func g() -> Any { S.self }
func h() -> S { S(j: 2) }
func i() -> KeyPath<S, Int> { \\S.j }
func j(@Wrap(-S.i, S.i) n: Int = S.i) {}
}
"""),
Example("""
struct S {
struct T {
static let i = 3
}
struct R {
static let j = S.T.i
}
static let h = S.T.i + S.R.j
}
"""),
Example("""
enum E {
case A
static func f() -> E { E.A }
static func g() -> E { E.f() }
}
"""),
Example("""
extension E {
class C {
static var i = 2
var j: Int { C.i }
var k: Int {
get { C.i }
set { C.i = newValue }
}
}
}
""", excludeFromDocumentation: true),
Example("""
class C {
typealias A = C
let d: C? = nil
var c: C { C() }
let b: [C] = [C]()
init() {}
func f(e: C) -> C {
let f: C = C()
return f
}
func g(a: [C]) -> [C] { a }
}
final class D {
typealias A = D
let c: D? = nil
var d: D { D() }
let b: [D] = [D]()
init() {}
func f(e: D) -> D {
let f: D = D()
return f
}
func g(a: [D]) -> [D] { a }
}
struct S {
typealias A = S
// let s: S? = nil // Struct cannot contain itself
var t: S { S() }
let b: [S] = [S]()
init() {}
func f(e: S) -> S {
let f: S = S()
return f
}
func g(a: [S]) -> [S] { a }
}
""", excludeFromDocumentation: true)
]
static let corrections = [
Example("""
final class CheckCellView: NSTableCellView {
@IBOutlet var checkButton: NSButton!
override func awakeFromNib() {
checkButton.action = #selector(CheckCellView.check(_:))
}
@objc func check(_ button: AnyObject?) {}
}
"""):
Example("""
final class CheckCellView: NSTableCellView {
@IBOutlet var checkButton: NSButton!
override func awakeFromNib() {
checkButton.action = #selector(Self.check(_:))
}
@objc func check(_ button: AnyObject?) {}
}
"""),
Example("""
struct S {
static let i = 1
static let j = S.i
let k = S . j
static func f(_ l: Int = S.i) -> Int { l*S.j }
func g() { S.i + S.f() + k }
}
"""): Example("""
struct S {
static let i = 1
static let j = Self.i
let k = Self . j
static func f(_ l: Int = Self.i) -> Int { l*Self.j }
func g() { Self.i + Self.f() + k }
}
""")
]
}

View File

@ -1,185 +0,0 @@
import SwiftSyntax
struct RedundantSelfInClosureRule: SwiftSyntaxRule, CorrectableRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "redundant_self_in_closure",
name: "Redundant Self in Closure",
description: "Explicit use of 'self' is not required",
kind: .style,
nonTriggeringExamples: RedundantSelfInClosureRuleExamples.nonTriggeringExamples,
triggeringExamples: RedundantSelfInClosureRuleExamples.triggeringExamples,
corrections: RedundantSelfInClosureRuleExamples.corrections
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
ContextVisitor()
}
func correct(file: SwiftLintFile) -> [Correction] {
let ranges = ContextVisitor()
.walk(file: file, handler: \.corrections)
.compactMap { file.stringView.NSRange(start: $0.start, end: $0.end) }
.filter { file.ruleEnabled(violatingRange: $0, for: self) != nil }
.reversed()
var corrections = [Correction]()
var contents = file.contents
for range in ranges {
let contentsNSString = contents.bridge()
contents = contentsNSString.replacingCharacters(in: range, with: "")
let location = Location(file: file, characterOffset: range.location)
corrections.append(Correction(ruleDescription: Self.description, location: location))
}
file.write(contents)
return corrections
}
}
private enum TypeDeclarationKind {
case likeStruct
case likeClass
}
private enum FunctionCallType {
case anonymousClosure
case function
}
private enum SelfCaptureKind {
case strong
case weak
case uncaptured
}
private class ContextVisitor: DeclaredIdentifiersTrackingVisitor {
private var typeDeclarations = Stack<TypeDeclarationKind>()
private var functionCalls = Stack<FunctionCallType>()
private var selfCaptures = Stack<SelfCaptureKind>()
private(set) var corrections = [(start: AbsolutePosition, end: AbsolutePosition)]()
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .extensionsAndProtocols }
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
typeDeclarations.push(.likeClass)
return .visitChildren
}
override func visitPost(_ node: ActorDeclSyntax) {
typeDeclarations.pop()
}
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
typeDeclarations.push(.likeClass)
return .visitChildren
}
override func visitPost(_ node: ClassDeclSyntax) {
typeDeclarations.pop()
}
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
if let selfItem = node.signature?.capture?.items?.first(where: \.capturesSelf) {
selfCaptures.push(selfItem.capturesWeakly ? .weak : .strong)
} else {
selfCaptures.push(.uncaptured)
}
return .visitChildren
}
override func visitPost(_ node: ClosureExprSyntax) {
guard let activeTypeDeclarationKind = typeDeclarations.peek(),
let activeFunctionCallType = functionCalls.peek(),
let activeSelfCaptureKind = selfCaptures.peek() else {
return
}
let localCorrections = ExplicitSelfVisitor(
typeDeclarationKind: activeTypeDeclarationKind,
functionCallType: activeFunctionCallType,
selfCaptureKind: activeSelfCaptureKind,
scope: scope
).walk(tree: node.statements, handler: \.corrections)
violations.append(contentsOf: localCorrections.map(\.start))
corrections.append(contentsOf: localCorrections)
selfCaptures.pop()
}
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
typeDeclarations.push(.likeStruct)
return .visitChildren
}
override func visitPost(_ node: EnumDeclSyntax) {
typeDeclarations.pop()
}
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
if node.calledExpression.is(ClosureExprSyntax.self) {
functionCalls.push(.anonymousClosure)
} else {
functionCalls.push(.function)
}
return .visitChildren
}
override func visitPost(_ node: FunctionCallExprSyntax) {
functionCalls.pop()
}
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
typeDeclarations.push(.likeStruct)
return .visitChildren
}
override func visitPost(_ node: StructDeclSyntax) {
typeDeclarations.pop()
}
}
private class ExplicitSelfVisitor: DeclaredIdentifiersTrackingVisitor {
private let typeDeclKind: TypeDeclarationKind
private let functionCallType: FunctionCallType
private let selfCaptureKind: SelfCaptureKind
private(set) var corrections = [(start: AbsolutePosition, end: AbsolutePosition)]()
init(typeDeclarationKind: TypeDeclarationKind,
functionCallType: FunctionCallType,
selfCaptureKind: SelfCaptureKind,
scope: Scope) {
self.typeDeclKind = typeDeclarationKind
self.functionCallType = functionCallType
self.selfCaptureKind = selfCaptureKind
super.init(scope: scope)
}
override func visitPost(_ node: MemberAccessExprSyntax) {
if !hasSeenDeclaration(for: node.name.text), node.isBaseSelf, isSelfRedundant {
corrections.append(
(start: node.positionAfterSkippingLeadingTrivia, end: node.dot.endPositionBeforeTrailingTrivia)
)
}
}
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
// Will be handled separately by the parent visitor.
.skipChildren
}
var isSelfRedundant: Bool {
typeDeclKind == .likeStruct
|| functionCallType == .anonymousClosure
|| selfCaptureKind == .strong && SwiftVersion.current >= .fiveDotThree
|| selfCaptureKind == .weak && SwiftVersion.current >= .fiveDotEight
}
}
private extension MemberAccessExprSyntax {
var isBaseSelf: Bool {
base?.as(IdentifierExprSyntax.self)?.isSelf == true
}
}

View File

@ -1,227 +0,0 @@
struct RedundantSelfInClosureRuleExamples {
static let nonTriggeringExamples = [
Example("""
struct S {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f {
x = 1
f { x = 1 }
g()
}
}
}
"""),
Example("""
class C {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f { [weak self] in
self?.x = 1
self?.g()
guard let self = self ?? C() else { return }
self?.x = 1
}
C().f { self.x = 1 }
f { [weak self] in if let self { x = 1 } }
}
}
"""),
Example("""
struct S {
var x = 0, error = 0, exception = 0
var y: Int?, z: Int?, u: Int, v: Int?, w: Int?
func f(_ work: @escaping (Int) -> Void) { work() }
func g(x: Int) {
f { u in
self.x = x
let x = 1
self.x = 2
if let y, let v {
self.y = 3
self.v = 1
}
guard let z else {
let v = 4
self.x = 5
self.v = 6
return
}
self.z = 7
while let v { self.v = 8 }
for w in [Int]() { self.w = 9 }
self.u = u
do {} catch { self.error = 10 }
do {} catch let exception { self.exception = 11 }
}
}
}
"""),
Example("""
enum E {
case a(Int)
case b(Int, Int)
}
struct S {
var x: E = .a(3), y: Int, z: Int
func f(_ work: @escaping () -> Void) { work() }
func g(x: Int) {
f {
switch x {
case let .a(y):
self.y = 1
case .b(let y, var z):
self.y = 2
self.z = 3
}
}
}
}
""")
]
static let triggeringExamples = [
Example("""
struct S {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f {
self.x = 1
if self.x == 1 { self.g() }
}
}
}
"""),
Example("""
class C {
var x = 0
func g() {
{
self.x = 1
self.g()
}()
}
}
"""),
Example("""
class C {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f { [self] in
self.x = 1
self.g()
f { self.x = 1 }
}
}
}
"""),
Example("""
class C {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f { [unowned self] in self.x = 1 }
f { [self = self] in self.x = 1 }
f { [s = self] in s.x = 1 }
}
}
"""),
Example("""
struct S {
var x = 0
var y: Int?, z: Int?, v: Int?, w: Int?
func f(_ work: @escaping () -> Void) { work() }
func g(w: Int, _ v: Int) {
f {
self.w = 1
self.x = 2
if let y { self.x = 3 }
else { self.y = 3 }
guard let z else {
self.z = 4
self.x = 5
return
}
self.y = 6
while let y { self.x = 7 }
for y in [Int]() { self.x = 8 }
self.v = 9
do {
let x = 10
self.x = 11
}
self.x = 12
}
}
}
"""),
Example("""
struct S {
func f(_ work: @escaping () -> Void) { work() }
func g() {
f { let g = self.g() }
}
}
""", excludeFromDocumentation: true)
] + triggeringCompilerSpecificExamples
static let corrections = [
Example("""
struct S {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f {
self.x = 1
if self.x == 1 { self.g() }
}
}
}
"""): Example("""
struct S {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f {
x = 1
if x == 1 { g() }
}
}
}
""")
]
#if compiler(>=5.8)
private static let triggeringCompilerSpecificExamples = [
Example("""
class C {
var x = 0
func f(_ work: @escaping () -> Void) { work() }
func g() {
f { [weak self] in
self?.x = 1
guard let self else { return }
self.x = 1
}
f { [weak self] in
self?.x = 1
if let self = self else { self.x = 1 }
self?.x = 1
}
f { [weak self] in
self?.x = 1
while let self else { self.x = 1 }
self?.x = 1
}
}
}
""")
]
#else
private static let triggeringCompilerSpecificExamples = [Example]()
#endif
}

View File

@ -1,118 +0,0 @@
import SwiftSyntax
struct SortedEnumCasesRule: ConfigurationProviderRule, SwiftSyntaxRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "sorted_enum_cases",
name: "Sorted Enum Cases",
description: "Enum cases should be sorted",
kind: .style,
nonTriggeringExamples: [
Example("""
enum foo {
case a
case b
case c
}
"""),
Example("""
enum foo {
case a, b, c
}
"""),
Example("""
enum foo {
case a
case b, c
}
"""),
Example("""
enum foo {
case a(foo: Foo)
case b(String), c
}
"""),
Example("""
@frozen
enum foo {
case b
case a
case c, f, d
}
""")
],
triggeringExamples: [
Example("""
enum foo {
case b
case a
case c
}
"""),
Example("""
enum foo {
case b, a, c
}
"""),
Example("""
enum foo {
case b, c
case a
}
"""),
Example("""
enum foo {
case a
case b, d, c
}
"""),
Example("""
enum foo {
case a(foo: Foo)
case c, b(String)
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension SortedEnumCasesRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] {
return .allExcept(EnumDeclSyntax.self)
}
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
guard !node.attributes.contains(attributeNamed: "frozen") else {
return .skipChildren
}
let cases = node.memberBlock.members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
let sortedCases = cases
.sorted(by: { $0.elements.first!.identifier.text < $1.elements.first!.identifier.text })
zip(sortedCases, cases).forEach { sortedCase, currentCase in
if sortedCase.elements.first?.identifier.text != currentCase.elements.first?.identifier.text {
violations.append(currentCase.positionAfterSkippingLeadingTrivia)
}
}
return .visitChildren
}
override func visitPost(_ node: EnumCaseDeclSyntax) {
let sortedElements = node.elements.sorted(by: { $0.identifier.text < $1.identifier.text })
zip(sortedElements, node.elements).forEach { sortedElement, currentElement in
if sortedElement.identifier.text != currentElement.identifier.text {
violations.append(currentElement.positionAfterSkippingLeadingTrivia)
}
}
}
}
}

View File

@ -1,233 +0,0 @@
internal struct SortedImportsRuleExamples {
private static let groupByAttributesConfiguration = ["grouping": "attributes"]
static let nonTriggeringExamples = [
Example("""
import AAA
import BBB
import CCC
import DDD
"""),
Example("""
import Alamofire
import API
"""),
Example("""
import labc
import Ldef
"""),
Example("""
import BBB
// comment
import AAA
import CCC
"""),
Example("""
@testable import AAA
import CCC
"""),
Example("""
import AAA
@testable import CCC
"""),
Example("""
import EEE.A
import FFF.B
#if os(Linux)
import DDD.A
import EEE.B
#else
import CCC
import DDD.B
#endif
import AAA
import BBB
"""),
Example("""
@testable import AAA
@testable import BBB
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
@testable import BBB
import AAA
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
@_exported import BBB
@testable import AAA
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
@_exported @testable import BBB
import AAA
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true)
]
static let triggeringExamples = [
Example("""
import AAA
import ZZZ
import BBB
import CCC
"""),
Example("""
import DDD
// comment
import CCC
import AAA
"""),
Example("""
@testable import CCC
import AAA
"""),
Example("""
import CCC
@testable import AAA
"""),
Example("""
import FFF.B
import EEE.A
#if os(Linux)
import DDD.A
import EEE.B
#else
import DDD.B
import CCC
#endif
import AAA
import BBB
"""),
Example("""
@testable import BBB
@testable import AAA
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
import AAA
@testable import BBB
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
import BBB
@testable import AAA
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
@testable import AAA
@_exported import BBB
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true),
Example("""
import AAA
@_exported @testable import BBB
""", configuration: groupByAttributesConfiguration, excludeFromDocumentation: true)
]
static let corrections = [
Example("""
import AAA
import ZZZ
import BBB
import CCC
"""):
Example("""
import AAA
import BBB
import CCC
import ZZZ
"""),
Example("""
import BBB // comment
import AAA
"""): Example("""
import AAA
import BBB // comment
"""),
Example("""
import BBB
// comment
import CCC
import AAA
"""):
Example("""
import BBB
// comment
import AAA
import CCC
"""),
Example("""
@testable import CCC
import AAA
"""): Example("""
import AAA
@testable import CCC
"""),
Example("""
import CCC
@testable import AAA
"""): Example("""
@testable import AAA
import CCC
"""),
Example("""
import FFF.B
import EEE.A
#if os(Linux)
import DDD.A
import EEE.B
#else
import DDD.B
import CCC
#endif
import AAA
import BBB
"""):
Example("""
import EEE.A
import FFF.B
#if os(Linux)
import DDD.A
import EEE.B
#else
import CCC
import DDD.B
#endif
import AAA
import BBB
"""),
Example("""
@testable import BBB
@testable import AAA
""", configuration: groupByAttributesConfiguration):
Example("""
@testable import AAA
@testable import BBB
"""),
Example("""
import AAA
@testable import BBB
""", configuration: groupByAttributesConfiguration):
Example("""
@testable import BBB
import AAA
"""),
Example("""
import BBB
@testable import AAA
""", configuration: groupByAttributesConfiguration):
Example("""
@testable import AAA
import BBB
"""),
Example("""
@testable import AAA
@_exported import BBB
""", configuration: groupByAttributesConfiguration):
Example("""
@_exported import BBB
@testable import AAA
"""),
Example("""
import AAA
@_exported @testable import BBB
""", configuration: groupByAttributesConfiguration):
Example("""
@_exported @testable import BBB
import AAA
""")
]
}

View File

@ -1,150 +0,0 @@
import SwiftSyntax
struct SuperfluousElseRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration<Self>(.warning)
static var description = RuleDescription(
identifier: "superfluous_else",
name: "Superfluous Else",
description: "Else branches should be avoided when the previous if-block exits the current scope",
kind: .style,
nonTriggeringExamples: [
Example("""
if i > 0 {
// comment
} else if i < 12 {
return 2
} else {
return 3
}
"""),
Example("""
if i > 0 {
let a = 1
if a > 1 {
// comment
} else {
return 1
}
// comment
} else {
return 3
}
"""),
Example("""
if i > 0 {
if a > 1 {
return 1
}
} else {
return 3
}
"""),
Example("""
if i > 0 {
if a > 1 {
if a > 1 {
// comment
} else {
return 1
}
}
} else {
return 3
}
""", excludeFromDocumentation: true)
],
triggeringExamples: [
Example("""
if i > 0 {
return 1
// comment
} else {
return 2
}
"""),
Example("""
if i > 0 {
return 1
} else if i < 12 {
return 2
} else if i > 18 {
return 3
}
"""),
Example("""
if i > 0 {
if i < 12 {
return 5
} else {
if i > 11 {
return 6
} else {
return 7
}
}
} else if i < 12 {
return 2
} else if i < 24 {
return 8
} else {
return 3
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
override func visitPost(_ node: IfExprSyntax) {
if node.violatesRule {
violations.append(node.ifKeyword.positionAfterSkippingLeadingTrivia)
}
}
}
private extension IfExprSyntax {
var violatesRule: Bool {
if elseKeyword == nil {
return false
}
let thenBodyReturns = lastStatementReturns(in: body)
if thenBodyReturns, let parent = parent?.as(IfExprSyntax.self) {
return parent.violatesRule
}
return thenBodyReturns
}
private var returnsInAllBranches: Bool {
guard lastStatementReturns(in: body) else {
return false
}
if case let .ifExpr(nestedIfStmt) = elseBody {
return nestedIfStmt.returnsInAllBranches
}
if case let .codeBlock(block) = elseBody {
return lastStatementReturns(in: block)
}
return false
}
private func lastStatementReturns(in block: CodeBlockSyntax) -> Bool {
guard let lastItem = block.statements.last?.as(CodeBlockItemSyntax.self)?.item else {
return false
}
if lastItem.is(ReturnStmtSyntax.self) {
return true
}
if let exprStmt = lastItem.as(ExpressionStmtSyntax.self),
let lastIfStmt = exprStmt.expression.as(IfExprSyntax.self) {
return lastIfStmt.returnsInAllBranches
}
return false
}
}

View File

@ -1,15 +0,0 @@
import SwiftIDEUtils
public extension SyntaxClassification {
// True if it is any kind of comment.
var isComment: Bool {
switch self {
case .lineComment, .docLineComment, .blockComment, .docBlockComment:
return true
case .none, .keyword, .identifier, .typeIdentifier, .operatorIdentifier, .dollarIdentifier, .integerLiteral,
.floatingLiteral, .stringLiteral, .stringInterpolationAnchor, .poundDirectiveKeyword, .buildConfigId,
.attribute, .objectLiteral, .editorPlaceholder, .regexLiteral:
return false
}
}
}

View File

@ -1,62 +0,0 @@
/// A basic stack type implementing the LIFO principle - only the last inserted element can be accessed and removed.
public struct Stack<Element> {
private var elements = [Element]()
/// Creates an empty `Stack`.
public init() {}
/// The number of elements in this stack.
public var count: Int {
elements.count
}
/// Pushes (appends) an element onto the stack.
///
/// - parameter element: The element to push onto the stack.
public mutating func push(_ element: Element) {
elements.append(element)
}
/// Removes and returns the last element of the stack.
///
/// - returns: The last element of the stack if the stack is not empty; otherwise, nil.
@discardableResult
public mutating func pop() -> Element? {
elements.popLast()
}
/// Returns the last element of the stack if the stack is not empty; otherwise, nil.
public func peek() -> Element? {
elements.last
}
/// Check whether the sequence contains an element that satisfies the given predicate.
///
/// - parameter predicate: A closure that takes an element of the sequence
/// and returns whether it represents a match.
/// - returns: `true` if the sequence contains an element that satisfies `predicate`.
public func contains(where predicate: (Element) -> Bool) -> Bool {
elements.contains(where: predicate)
}
/// Modify the last element.
///
/// - parameter modifier: A function to applied to the last element to modify it in place.
public mutating func modifyLast(by modifier: (inout Element) -> Void) {
if elements.isNotEmpty {
modifier(&elements[count - 1])
}
}
}
extension Stack: CustomDebugStringConvertible where Element == CustomDebugStringConvertible {
public var debugDescription: String {
let intermediateElements = count > 1 ? elements[1 ..< count - 1] : []
return """
Stack with \(count) elements:
first: \(elements.first?.debugDescription ?? "")
intermediate: \(intermediateElements.map(\.debugDescription).joined(separator: ", "))
last: \(peek()?.debugDescription ?? "")
"""
}
}

View File

@ -1,34 +0,0 @@
/// A rule configuration that allows to disable (`off`) an option of a rule or specify its severity level in which
/// case it's active.
public struct ChildOptionSeverityConfiguration<Parent: Rule>: RuleConfiguration, Equatable {
/// Configuration with a warning severity.
public static var error: Self { Self(optionSeverity: .error) }
/// Configuration with an error severity.
public static var warning: Self { Self(optionSeverity: .warning) }
/// Configuration disabling an option.
public static var off: Self { Self(optionSeverity: .off) }
enum ChildOptionSeverity: String {
case warning, error, off
}
private var optionSeverity: ChildOptionSeverity
public var consoleDescription: String {
optionSeverity.rawValue
}
/// The `ChildOptionSeverityConfiguration` mapped to a usually used `ViolationSeverity`. It's `nil` if the option
/// is set to `off`.
public var severity: ViolationSeverity? {
ViolationSeverity(rawValue: optionSeverity.rawValue)
}
public mutating func apply(configuration: Any) throws {
guard let configString = configuration as? String,
let optionSeverity = ChildOptionSeverity(rawValue: configString.lowercased()) else {
throw Issue.unknownConfiguration(ruleID: Parent.description.identifier)
}
self.optionSeverity = optionSeverity
}
}

View File

@ -1,2 +0,0 @@
@_spi(TestHelper)
public typealias ConfigurationRuleWrapper = (rule: Rule, initializedWithNonEmptyConfiguration: Bool)

View File

@ -1,33 +0,0 @@
import Foundation
/// Utility to measure the time spent in each custom rule.
public final class CustomRuleTimer {
private let lock = NSLock()
private var ruleIDForTimes = [String: [TimeInterval]]()
private var shouldRecord = false
/// Singleton.
public static let shared = CustomRuleTimer()
/// Tell the timer it should record time spent in rules.
public func activate() {
shouldRecord = true
}
/// Return all time spent for each custom rule, keyed by rule ID.
public func dump() -> [String: TimeInterval] {
ruleIDForTimes.mapValues { $0.reduce(0, +) }
}
/// Register time spent evaluating a rule with the specified ID.
///
/// - parameter time: The time interval spent evaluating this rule ID.
/// - parameter ruleID: The ID of the rule that was evaluated.
func register(time: TimeInterval, forRuleID ruleID: String) {
guard shouldRecord else { return }
lock.lock()
defer { lock.unlock() }
ruleIDForTimes[ruleID, default: []].append(time)
}
}

View File

@ -1,119 +0,0 @@
import Foundation
/// All possible SwiftLint issues which are printed as warnings by default.
public enum Issue: LocalizedError, Equatable {
/// The configuration didn't match internal expectations.
case unknownConfiguration(ruleID: String)
/// Rule is listed multiple times in the configuration.
case listedMultipleTime(ruleID: String, times: Int)
/// An identifier `old` has been renamed to `new`.
case renamedIdentifier(old: String, new: String)
/// Configuration for a rule is invalid.
case invalidConfiguration(ruleID: String)
/// Some configuration keys are invalid.
case invalidConfigurationKeys([String])
/// A generic warning specified by a string.
case genericWarning(String)
/// A generic error specified by a string.
case genericError(String)
/// A deprecation warning for a rule.
case ruleDeprecated(ruleID: String)
/// The initial configuration file was not found.
case initialFileNotFound(path: String)
/// The file at `path` is not readable or cannot be opened.
case fileNotReadable(path: String?, ruleID: String)
/// The file at `path` is not writable.
case fileNotWritable(path: String)
/// The file at `path` cannot be indexed by a specific rule.
case indexingError(path: String?, ruleID: String)
/// No arguments were provided to compile a file at `path` within a specific rule.
case missingCompilerArguments(path: String?, ruleID: String)
/// Cursor information cannot be extracted from a specific location.
case missingCursorInfo(path: String?, ruleID: String)
/// An error that occurred when parsing YAML.
case yamlParsing(String)
/// Wraps any `Error` into a `SwiftLintError.genericWarning` if it is not already a `SwiftLintError`.
///
/// - parameter error: Any `Error`.
///
/// - returns: A `SwiftLintError.genericWarning` containig the message of the `error` argument.
static func wrap(error: Error) -> Self {
error as? Issue ?? Self.genericWarning(error.localizedDescription)
}
/// Make this issue an error.
var asError: Self {
Self.genericError(message)
}
/// The issues description which is ready to be printed to the console.
var errorDescription: String {
switch self {
case .genericError:
return "error: \(message)"
case .genericWarning:
return "warning: \(message)"
default:
return Self.genericWarning(message).errorDescription
}
}
/// Print the issue to the console.
public func print() {
queuedPrintError(errorDescription)
}
private var message: String {
switch self {
case let .unknownConfiguration(id):
return "Invalid configuration for '\(id)' rule. Falling back to default."
case let .listedMultipleTime(id, times):
return "'\(id)' is listed \(times) times in the configuration."
case let .renamedIdentifier(old, new):
return "'\(old)' has been renamed to '\(new)' and will be completely removed in a future release."
case let .invalidConfiguration(id):
return "Invalid configuration for '\(id)'. Falling back to default."
case let .invalidConfigurationKeys(keys):
return "Configuration contains invalid keys \(keys.joined(separator: ", "))."
case let .genericWarning(message), let .genericError(message):
return message
case let .ruleDeprecated(id):
return """
The `\(id)` rule is now deprecated and will be \
completely removed in a future release.
"""
case let .initialFileNotFound(path):
return "Could not read file at path '\(path)'."
case let .fileNotReadable(path, id):
return "Cannot open or read file at path '\(path ?? "...")' within '\(id)' rule."
case let .fileNotWritable(path):
return "Cannot write to file at path '\(path)'."
case let .indexingError(path, id):
return "Cannot index file at path '\(path ?? "...")' within '\(id)' rule."
case let .missingCompilerArguments(path, id):
return """
Attempted to lint file at path '\(path ?? "...")' within '\(id)' rule \
without any compiler arguments.
"""
case let .missingCursorInfo(path, id):
return "Cannot get cursor info from file at path '\(path ?? "...")' within '\(id)' rule."
case let .yamlParsing(message):
return "Cannot parse YAML file: \(message)"
}
}
}

View File

@ -1,20 +0,0 @@
// Generated using Sourcery 2.0.2 https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
/// The reporters list containing all the reporters built into SwiftLint.
public let reportersList: [Reporter.Type] = [
CSVReporter.self,
CheckstyleReporter.self,
CodeClimateReporter.self,
EmojiReporter.self,
GitHubActionsLoggingReporter.self,
GitLabJUnitReporter.self,
HTMLReporter.self,
JSONReporter.self,
JUnitReporter.self,
MarkdownReporter.self,
RelativePathReporter.self,
SonarQubeReporter.self,
SummaryReporter.self,
XcodeReporter.self
]

View File

@ -1,32 +0,0 @@
/// Container to register and look up SwiftLint rules.
public final class RuleRegistry {
private var registeredRules = [Rule.Type]()
/// Shared rule registry instance.
public static let shared = RuleRegistry()
/// Rule list associated with this registry. Lazily created, and
/// immutable once looked up.
///
/// - note: Adding registering more rules after this was first
/// accessed will not work.
public private(set) lazy var list = RuleList(rules: registeredRules)
private init() {}
/// Register rules.
///
/// - parameter rules: The rules to register.
public func register(rules: [Rule.Type]) {
registeredRules.append(contentsOf: rules)
}
/// Look up a rule for a given ID.
///
/// - parameter id: The ID for the rule to look up.
///
/// - returns: The rule matching the specified ID, if one was found.
public func rule(forID id: String) -> Rule.Type? {
return list.list[id]
}
}

View File

@ -1,34 +0,0 @@
/// A rule configuration that allows specifying the desired severity level for violations.
public struct SeverityConfiguration<Parent: Rule>: SeverityBasedRuleConfiguration, Equatable {
/// Configuration with a warning severity.
public static var error: Self { Self(.error) }
/// Configuration with an error severity.
public static var warning: Self { Self(.warning) }
public var consoleDescription: String {
return severity.rawValue
}
var severity: ViolationSeverity
public var severityConfiguration: SeverityConfiguration {
self
}
/// Create a `SeverityConfiguration` with the specified severity.
///
/// - parameter severity: The severity that should be used when emitting violations.
public init(_ severity: ViolationSeverity) {
self.severity = severity
}
public mutating func apply(configuration: Any) throws {
let configString = configuration as? String
let configDict = configuration as? [String: Any]
guard let severityString: String = configString ?? configDict?["severity"] as? String,
let severity = ViolationSeverity(rawValue: severityString.lowercased()) else {
throw Issue.unknownConfiguration(ruleID: Parent.description.identifier)
}
self.severity = severity
}
}

View File

@ -1,6 +0,0 @@
/// Interface providing access to a cache description.
public protocol CacheDescriptionProvider {
/// The cache description which will be used to determine if a previous
/// cached value is still valid given the new cache value.
var cacheDescription: String { get }
}

View File

@ -1,178 +0,0 @@
/// Type-erased protocol used to check whether a rule is collectable.
public protocol AnyCollectingRule: Rule { }
/// A rule that requires knowledge of all other files being linted.
public protocol CollectingRule: AnyCollectingRule {
/// The kind of information to collect for each file being linted for this rule.
associatedtype FileInfo
/// Collects information for the specified file, to be analyzed by a `CollectedLinter`.
///
/// - parameter file: The file for which to collect info.
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
///
/// - returns: The collected file information.
func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> FileInfo
/// Collects information for the specified file, to be analyzed by a `CollectedLinter`.
///
/// - parameter file: The file for which to collect info.
///
/// - returns: The collected file information.
func collectInfo(for file: SwiftLintFile) -> FileInfo
/// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's
/// expectations.
///
/// - parameter file: The file for which to execute the rule.
/// - parameter collectedInfo: All collected info for all files.
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
///
/// - returns: All style violations to the rule's expectations.
func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo],
compilerArguments: [String]) -> [StyleViolation]
/// Executes the rule on a file after collecting file info for all files and returns any violations to the rule's
/// expectations.
///
/// - parameter file: The file for which to execute the rule.
/// - parameter collectedInfo: All collected info for all files.
///
/// - returns: All style violations to the rule's expectations.
func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [StyleViolation]
}
public extension CollectingRule {
func collectInfo(for file: SwiftLintFile, into storage: RuleStorage, compilerArguments: [String]) {
storage.collect(info: collectInfo(for: file, compilerArguments: compilerArguments),
for: file, in: self)
}
func validate(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [StyleViolation] {
guard let info = storage.collectedInfo(for: self) else {
queuedFatalError("Attempt to validate a CollectingRule before collecting info for it")
}
return validate(file: file, collectedInfo: info, compilerArguments: compilerArguments)
}
func collectInfo(for file: SwiftLintFile, compilerArguments: [String]) -> FileInfo {
return collectInfo(for: file)
}
func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo],
compilerArguments: [String]) -> [StyleViolation] {
return validate(file: file, collectedInfo: collectedInfo)
}
func validate(file: SwiftLintFile) -> [StyleViolation] {
queuedFatalError("Must call `validate(file:collectedInfo:)` for CollectingRule")
}
func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] {
queuedFatalError("Must call `validate(file:collectedInfo:compilerArguments:)` for CollectingRule")
}
}
public extension CollectingRule where Self: AnalyzerRule {
func collectInfo(for file: SwiftLintFile) -> FileInfo {
queuedFatalError(
"Must call `collect(infoFor:compilerArguments:)` for AnalyzerRule & CollectingRule"
)
}
func validate(file: SwiftLintFile) -> [StyleViolation] {
queuedFatalError(
"Must call `validate(file:collectedInfo:compilerArguments:)` for AnalyzerRule & CollectingRule"
)
}
func validate(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [StyleViolation] {
queuedFatalError(
"Must call `validate(file:collectedInfo:compilerArguments:)` for AnalyzerRule & CollectingRule"
)
}
}
/// A `CollectingRule` that is also a `CorrectableRule`.
@_spi(TestHelper)
public protocol CollectingCorrectableRule: CollectingRule, CorrectableRule {
/// Attempts to correct the violations to this rule in the specified file after collecting file info for all files
/// and returns all corrections that were applied.
///
/// - note: This function is called by the linter and is always implemented in extensions.
///
/// - parameter file: The file for which to execute the rule.
/// - parameter collectedInfo: All collected info.
/// - parameter compilerArguments: The compiler arguments needed to compile this file.
///
/// - returns: All corrections that were applied.
func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo],
compilerArguments: [String]) -> [Correction]
/// Attempts to correct the violations to this rule in the specified file after collecting file info for all files
/// and returns all corrections that were applied.
///
/// - note: This function is called by the linter and is always implemented in extensions.
///
/// - parameter file: The file for which to execute the rule.
/// - parameter collectedInfo: All collected info.
///
/// - returns: All corrections that were applied.
func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [Correction]
}
@_spi(TestHelper)
public extension CollectingCorrectableRule {
func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo],
compilerArguments: [String]) -> [Correction] {
return correct(file: file, collectedInfo: collectedInfo)
}
func correct(file: SwiftLintFile, using storage: RuleStorage, compilerArguments: [String]) -> [Correction] {
guard let info = storage.collectedInfo(for: self) else {
queuedFatalError("Attempt to correct a CollectingRule before collecting info for it")
}
return correct(file: file, collectedInfo: info, compilerArguments: compilerArguments)
}
func correct(file: SwiftLintFile) -> [Correction] {
queuedFatalError("Must call `correct(file:collectedInfo:)` for AnalyzerRule")
}
func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] {
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
}
}
public extension CollectingCorrectableRule where Self: AnalyzerRule {
func correct(file: SwiftLintFile) -> [Correction] {
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
}
func correct(file: SwiftLintFile, compilerArguments: [String]) -> [Correction] {
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
}
func correct(file: SwiftLintFile, collectedInfo: [SwiftLintFile: FileInfo]) -> [Correction] {
queuedFatalError("Must call `correct(file:collectedInfo:compilerArguments:)` for AnalyzerRule")
}
}
public extension ConfigurationProviderRule {
init(configuration: Any) throws {
self.init()
try self.configuration.apply(configuration: configuration)
}
func isEqualTo(_ rule: Rule) -> Bool {
if let rule = rule as? Self {
return configuration.isEqualTo(rule.configuration)
}
return false
}
var configurationDescription: String {
return configuration.consoleDescription
}
}
// MARK: - == Implementations
/// :nodoc:
public extension Array where Element == Rule {
static func == (lhs: Array, rhs: Array) -> Bool {
if lhs.count != rhs.count { return false }
return !zip(lhs, rhs).contains { !$0.0.isEqualTo($0.1) }
}
}

View File

@ -1,40 +0,0 @@
/// An interface for reporting violations as strings.
public protocol Reporter: CustomStringConvertible {
/// The unique identifier for this reporter.
static var identifier: String { get }
/// Whether or not this reporter can output incrementally as violations are found or if all violations must be
/// collected before generating the report.
static var isRealtime: Bool { get }
/// A more detailed description of the reporter's output.
static var description: String { get }
/// For CustomStringConvertible conformance.
var description: String { get }
/// Return a string with the report for the specified violations.
///
/// - parameter violations: The violations to report.
///
/// - returns: The report.
static func generateReport(_ violations: [StyleViolation]) -> String
}
public extension Reporter {
/// For CustomStringConvertible conformance.
var description: String { Self.description }
}
/// Returns the reporter with the specified identifier. Traps if the specified identifier doesn't correspond to any
/// known reporters.
///
/// - parameter identifier: The identifier corresponding to the reporter.
///
/// - returns: The reporter type.
public func reporterFrom(identifier: String) -> Reporter.Type {
guard let reporter = reportersList.first(where: { $0.identifier == identifier }) else {
queuedFatalError("No reporter with identifier '\(identifier)' available.")
}
return reporter
}

View File

@ -1,36 +0,0 @@
/// Reports violations in a format that's both fun and easy to read.
public struct EmojiReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "emoji"
public static let isRealtime = false
public static let description = "Reports violations in the format that's both fun and easy to read."
public static func generateReport(_ violations: [StyleViolation]) -> String {
violations
.group { $0.location.file ?? "Other" }
.sorted { $0.key < $1.key }
.map(report)
.joined(separator: "\n")
}
// MARK: - Private
private static func report(for file: String, with violations: [StyleViolation]) -> String {
let issueList = violations
.sorted { $0.severity == $1.severity ? $0.location > $1.location : $0.severity > $1.severity }
.map { violation in
let emoji = violation.severity == .error ? "⛔️" : "⚠️"
var lineString = ""
if let line = violation.location.line {
lineString = "Line \(line): "
}
return "\(emoji) \(lineString)\(violation.reason) (\(violation.ruleIdentifier))"
}
.joined(separator: "\n")
return """
\(file)
\(issueList)
"""
}
}

View File

@ -1,168 +0,0 @@
import Foundation
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
return formatter
}()
/// Reports violations as HTML.
public struct HTMLReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "html"
public static let isRealtime = false
public static let description = "Reports violations as HTML."
public static func generateReport(_ violations: [StyleViolation]) -> String {
return generateReport(violations, swiftlintVersion: Version.current.value,
dateString: formatter.string(from: Date()))
}
// MARK: - Internal
// swiftlint:disable:next function_body_length
internal static func generateReport(_ violations: [StyleViolation], swiftlintVersion: String,
dateString: String) -> String {
let rows = violations.enumerated()
.map { generateSingleRow(for: $1, at: $0 + 1) }
.joined(separator: "\n")
let fileCount = Set(violations.compactMap({ $0.location.file })).count
let warningCount = violations.filter({ $0.severity == .warning }).count
let errorCount = violations.filter({ $0.severity == .error }).count
return """
<!doctype html>
<html>
\t<head>
\t\t<meta charset="utf-8" />
\t\t<meta name="viewport" content="width=device-width, initial-scale=1.0" />
\t\t
\t\t<style type="text/css">
\t\t\tbody {
\t\t\t\tfont-family: Arial, Helvetica, sans-serif;
\t\t\t\tfont-size: 0.9rem;
\t\t\t}
\t\t\t
\t\t\ttable {
\t\t\t\tborder: 1px solid gray;
\t\t\t\tborder-collapse: collapse;
\t\t\t\t-moz-box-shadow: 3px 3px 4px #AAA;
\t\t\t\t-webkit-box-shadow: 3px 3px 4px #AAA;
\t\t\t\tbox-shadow: 3px 3px 4px #AAA;
\t\t\t\tvertical-align: top;
\t\t\t\theight: 64px;
\t\t\t}
\t\t\t
\t\t\ttd, th {
\t\t\t\tborder: 1px solid #D3D3D3;
\t\t\t\tpadding: 5px 10px 5px 10px;
\t\t\t}
\t\t\t
\t\t\tth {
\t\t\t\tborder-bottom: 1px solid gray;
\t\t\t\tbackground-color: rgba(41,52,92,0.313);
\t\t\t}
\t\t\t
\t\t\t.error, .warning {
\t\t\t\ttext-align: center;
\t\t\t}
\t\t\t
\t\t\t.error {
\t\t\t\tbackground-color: #FF9D92;
\t\t\t\tcolor: #7F0800;
\t\t\t}
\t\t\t
\t\t\t.warning {
\t\t\t\tbackground-color: #FFF59E;
\t\t\t\tcolor: #7F7000;
\t\t\t}
\t\t</style>
\t\t
\t\t<title>SwiftLint Report</title>
\t</head>
\t<body>
\t\t<h1>SwiftLint Report</h1>
\t\t
\t\t<hr />
\t\t
\t\t<h2>Violations</h2>
\t\t
\t\t<table>
\t\t\t<thead>
\t\t\t\t<tr>
\t\t\t\t\t<th style="width: 60pt;">
\t\t\t\t\t\t<b>Serial No.</b>
\t\t\t\t\t</th>
\t\t\t\t\t<th style="width: 500pt;">
\t\t\t\t\t\t<b>File</b>
\t\t\t\t\t</th>
\t\t\t\t\t<th style="width: 60pt;">
\t\t\t\t\t\t<b>Location</b>
\t\t\t\t\t</th>
\t\t\t\t\t<th style="width: 60pt;">
\t\t\t\t\t\t<b>Severity</b>
\t\t\t\t\t</th>
\t\t\t\t\t<th style="width: 500pt;">
\t\t\t\t\t\t<b>Message</b>
\t\t\t\t\t</th>
\t\t\t\t</tr>
\t\t\t</thead>
\t\t\t<tbody>
\(rows)
\t\t\t</tbody>
\t\t</table>
\t\t
\t\t<br/>
\t\t
\t\t<h2>Summary</h2>
\t\t
\t\t<table>
\t\t\t<tbody>
\t\t\t\t<tr>
\t\t\t\t\t<td>Total files with violations</td>
\t\t\t\t\t<td>\(fileCount)</td>
\t\t\t\t</tr>
\t\t\t\t<tr>
\t\t\t\t\t<td>Total warnings</td>
\t\t\t\t\t<td>\(warningCount)</td>
\t\t\t\t</tr>
\t\t\t\t<tr>
\t\t\t\t\t<td>Total errors</td>
\t\t\t\t\t<td>\(errorCount)</td>
\t\t\t\t</tr>
\t\t\t</tbody>
\t\t</table>
\t\t
\t\t<hr />
\t\t
\t\t<p>
\t\t\tCreated with
\t\t\t<a href="https://github.com/realm/SwiftLint"><b>SwiftLint</b></a>
\t\t\t\(swiftlintVersion) on \(dateString)
\t\t</p>
\t</body>
</html>
"""
}
// MARK: - Private
private static func generateSingleRow(for violation: StyleViolation, at index: Int) -> String {
let severity: String = violation.severity.rawValue.capitalized
let location = violation.location
let file: String = (violation.location.relativeFile ?? "<nopath>").escapedForXML()
let line: Int = location.line ?? 0
let character: Int = location.character ?? 0
return """
\t\t\t\t<tr>
\t\t\t\t\t<td style="text-align: right;">\(index)</td>
\t\t\t\t\t<td>\(file)</td>
\t\t\t\t\t<td style="text-align: center;">\(line):\(character)</td>
\t\t\t\t\t<td class="\(severity.lowercased())">\(severity)</td>
\t\t\t\t\t<td>\(violation.reason.escapedForXML())</td>
\t\t\t\t</tr>
"""
}
}

View File

@ -1,36 +0,0 @@
/// Reports violations as JUnit XML.
public struct JUnitReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "junit"
public static let isRealtime = false
public static let description = "Reports violations as JUnit XML."
public static func generateReport(_ violations: [StyleViolation]) -> String {
let warningCount = violations.filter({ $0.severity == .warning }).count
let errorCount = violations.filter({ $0.severity == .error }).count
return """
<?xml version="1.0" encoding="utf-8"?>
<testsuites failures="\(warningCount)" errors="\(errorCount)">
\t<testsuite failures="\(warningCount)" errors="\(errorCount)">
\(violations.map(testCase(for:)).joined(separator: "\n"))
\t</testsuite>
</testsuites>
"""
}
private static func testCase(for violation: StyleViolation) -> String {
let fileName = (violation.location.file ?? "<nopath>").escapedForXML()
let reason = violation.reason.escapedForXML()
let severity = violation.severity.rawValue.capitalized
let lineNumber = String(violation.location.line ?? 0)
let message = severity + ":" + "Line:" + lineNumber
return """
\t\t<testcase classname='Formatting Test' name='\(fileName)'>
\t\t\t<failure message='\(reason)'>\(message)</failure>
\t\t</testcase>
"""
}
}

View File

@ -1,31 +0,0 @@
/// Reports violations with relative paths.
public struct RelativePathReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "relative-path"
public static let isRealtime = true
public static let description = "Reports violations with relative paths."
public static func generateReport(_ violations: [StyleViolation]) -> String {
return violations.map(generateForSingleViolation).joined(separator: "\n")
}
/// Generates a report for a single violation.
///
/// - parameter violation: The violation to report.
///
/// - returns: The report for a single violation.
internal static func generateForSingleViolation(_ violation: StyleViolation) -> String {
// {relative_path_to_file}{:line}{:character}: {error,warning}: {content}
return [
"\(violation.location.relativeFile ?? "<nopath>")",
":\(violation.location.line ?? 1)",
":\(violation.location.character ?? 1): ",
"\(violation.severity.rawValue): ",
"\(violation.ruleName) Violation: ",
violation.reason,
" (\(violation.ruleIdentifier))"
].joined()
}
}

View File

@ -1,126 +0,0 @@
import Foundation
import SwiftyTextTable
/// Reports a summary table of all violations
public struct SummaryReporter: Reporter {
// MARK: - Reporter Conformance
public static let identifier = "summary"
public static let isRealtime = false
public static let description = "Reports a summary table of all violations."
public static func generateReport(_ violations: [StyleViolation]) -> String {
TextTable(violations: violations).renderWithExtraSeparator()
}
}
// MARK: - SwiftyTextTable
private extension TextTable {
// swiftlint:disable:next function_body_length
init(violations: [StyleViolation]) {
let numberOfWarningsHeader = "warnings"
let numberOfErrorsHeader = "errors"
let numberOfViolationsHeader = "total violations"
let numberOfFilesHeader = "number of files"
let columns = [
TextTableColumn(header: "rule identifier"),
TextTableColumn(header: "opt-in"),
TextTableColumn(header: "correctable"),
TextTableColumn(header: "custom"),
TextTableColumn(header: numberOfWarningsHeader),
TextTableColumn(header: numberOfErrorsHeader),
TextTableColumn(header: numberOfViolationsHeader),
TextTableColumn(header: numberOfFilesHeader)
]
self.init(columns: columns)
let ruleIdentifiersToViolationsMap = violations.group { $0.ruleIdentifier }
let sortedRuleIdentifiers = ruleIdentifiersToViolationsMap.keys.sorted {
let count1 = ruleIdentifiersToViolationsMap[$0]?.count ?? 0
let count2 = ruleIdentifiersToViolationsMap[$1]?.count ?? 0
if count1 > count2 {
return true
} else if count1 == count2 {
return $0 < $1
}
return false
}
var totalNumberOfWarnings = 0
var totalNumberOfErrors = 0
for ruleIdentifier in sortedRuleIdentifiers {
guard let ruleIdentifier = ruleIdentifiersToViolationsMap[ruleIdentifier]?.first?.ruleIdentifier else {
continue
}
let rule = RuleRegistry.shared.rule(forID: ruleIdentifier)
let violations = ruleIdentifiersToViolationsMap[ruleIdentifier]
let numberOfWarnings = violations?.filter { $0.severity == .warning }.count ?? 0
let numberOfErrors = violations?.filter { $0.severity == .error }.count ?? 0
let numberOfViolations = numberOfWarnings + numberOfErrors
totalNumberOfWarnings += numberOfWarnings
totalNumberOfErrors += numberOfErrors
let ruleViolations = ruleIdentifiersToViolationsMap[ruleIdentifier] ?? []
let numberOfFiles = Set(ruleViolations.map { $0.location.file }).count
addRow(values: [
ruleIdentifier,
rule is OptInRule.Type ? "yes" : "no",
rule is CorrectableRule.Type ? "yes" : "no",
rule == nil ? "yes" : "no",
numberOfWarnings.formattedString.leftPadded(forHeader: numberOfWarningsHeader),
numberOfErrors.formattedString.leftPadded(forHeader: numberOfErrorsHeader),
numberOfViolations.formattedString.leftPadded(forHeader: numberOfViolationsHeader),
numberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader)
])
}
let totalNumberOfViolations = totalNumberOfWarnings + totalNumberOfErrors
let totalNumberOfFiles = Set(violations.map { $0.location.file }).count
addRow(values: [
"Total",
"",
"",
"",
totalNumberOfWarnings.formattedString.leftPadded(forHeader: numberOfWarningsHeader),
totalNumberOfErrors.formattedString.leftPadded(forHeader: numberOfErrorsHeader),
totalNumberOfViolations.formattedString.leftPadded(forHeader: numberOfViolationsHeader),
totalNumberOfFiles.formattedString.leftPadded(forHeader: numberOfFilesHeader)
])
}
func renderWithExtraSeparator() -> String {
var output = render()
var lines = output.components(separatedBy: "\n")
if lines.count > 5, let lastLine = lines.last {
lines.insert(lastLine, at: lines.count - 2)
output = lines.joined(separator: "\n")
}
return output
}
}
private extension String {
func leftPadded(forHeader header: String) -> String {
let headerCount = header.count - self.count
if headerCount > 0 {
return String(repeating: " ", count: headerCount) + self
}
return self
}
}
private extension Int {
private static var numberFormatter: NumberFormatter = {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
return numberFormatter
}()
var formattedString: String {
// swiftlint:disable:next legacy_objc_type
Int.numberFormatter.string(from: NSNumber(value: self)) ?? ""
}
}

View File

@ -1,5 +0,0 @@
/// The rule list containing all available rules built into SwiftLintCore.
public let coreRules: [Rule.Type] = [
CustomRules.self,
SuperfluousDisableCommandRule.self
]

View File

@ -1,32 +0,0 @@
@_spi(TestHelper)
public struct SuperfluousDisableCommandRule: ConfigurationProviderRule, SourceKitFreeRule {
public var configuration = SeverityConfiguration<Self>(.warning)
public init() {}
public static let description = RuleDescription(
identifier: "superfluous_disable_command",
name: "Superfluous Disable Command",
description: """
SwiftLint 'disable' commands are superfluous when the disabled rule would not have triggered a violation \
in the disabled region. Use " - " if you wish to document a command.
""",
kind: .lint
)
public func validate(file: SwiftLintFile) -> [StyleViolation] {
// This rule is implemented in Linter.swift
return []
}
func reason(for rule: Rule.Type) -> String {
"""
SwiftLint rule '\(rule.description.identifier)' did not trigger a violation in the disabled region; \
remove the disable command
"""
}
func reason(forNonExistentRule rule: String) -> String {
return "'\(rule)' is not a valid SwiftLint rule; remove it from the disable command"
}
}

View File

@ -1,107 +0,0 @@
import SwiftSyntax
/// A specialized `ViolationsSyntaxVisitor` that tracks declared identifiers per scope while traversing the AST.
open class DeclaredIdentifiersTrackingVisitor: ViolationsSyntaxVisitor {
/// A type that remembers the declared identifers (in order) up to the current position in the code.
public typealias Scope = Stack<Set<String>>
/// The hierarchical stack of identifiers declared up to the current position in the code.
public private(set) var scope: Scope
/// Initializer.
///
/// - parameter scope: A (potentially already pre-filled) scope to collect identifers into.
public init(scope: Scope = Scope()) {
self.scope = scope
super.init(viewMode: .sourceAccurate)
}
/// Indicate whether a given identifier is in scope.
///
/// - parameter identifier: An identifier.
public func hasSeenDeclaration(for identifier: String) -> Bool {
scope.contains { $0.contains(identifier) }
}
// swiftlint:disable:next cyclomatic_complexity
override open func visit(_ node: CodeBlockItemListSyntax) -> SyntaxVisitorContinueKind {
guard let parent = node.parent, !parent.is(SourceFileSyntax.self), let grandParent = parent.parent else {
return .visitChildren
}
scope.openChildScope()
if let ifStmt = grandParent.as(IfExprSyntax.self), parent.keyPathInParent != \IfExprSyntax.elseBody {
collectIdentifiers(fromConditions: ifStmt.conditions)
} else if let whileStmt = grandParent.as(WhileStmtSyntax.self) {
collectIdentifiers(fromConditions: whileStmt.conditions)
} else if let pattern = grandParent.as(ForInStmtSyntax.self)?.pattern {
collectIdentifiers(fromPattern: pattern)
} else if let parameters = grandParent.as(FunctionDeclSyntax.self)?.signature.input.parameterList {
parameters.forEach { scope.addToCurrentScope(($0.secondName ?? $0.firstName).text) }
} else if let input = parent.as(ClosureExprSyntax.self)?.signature?.input {
switch input {
case let .input(parameters):
parameters.parameterList.forEach { scope.addToCurrentScope(($0.secondName ?? $0.firstName).text) }
case let .simpleInput(parameters):
parameters.forEach { scope.addToCurrentScope($0.name.text) }
}
} else if let switchCase = parent.as(SwitchCaseSyntax.self)?.label.as(SwitchCaseLabelSyntax.self) {
switchCase.caseItems
.compactMap { $0.pattern.as(ValueBindingPatternSyntax.self)?.valuePattern ?? $0.pattern }
.compactMap { $0.as(ExpressionPatternSyntax.self)?.expression.asFunctionCall }
.compactMap { $0.argumentList.as(TupleExprElementListSyntax.self) }
.flatMap { $0 }
.compactMap { $0.expression.as(UnresolvedPatternExprSyntax.self) }
.compactMap { $0.pattern.as(ValueBindingPatternSyntax.self)?.valuePattern ?? $0.pattern }
.compactMap { $0.as(IdentifierPatternSyntax.self) }
.forEach { scope.addToCurrentScope($0.identifier.text) }
} else if let catchClause = grandParent.as(CatchClauseSyntax.self) {
if let items = catchClause.catchItems {
items
.compactMap { $0.pattern?.as(ValueBindingPatternSyntax.self)?.valuePattern }
.forEach(collectIdentifiers(fromPattern:))
} else {
// A catch clause without explicit catch items has an implicit `error` variable in scope.
scope.addToCurrentScope("error")
}
}
return .visitChildren
}
override open func visitPost(_ node: CodeBlockItemListSyntax) {
scope.pop()
}
override open func visitPost(_ node: VariableDeclSyntax) {
if node.parent?.is(MemberDeclListItemSyntax.self) != true {
for binding in node.bindings {
collectIdentifiers(fromPattern: binding.pattern)
}
}
}
override open func visitPost(_ node: GuardStmtSyntax) {
collectIdentifiers(fromConditions: node.conditions)
}
private func collectIdentifiers(fromConditions conditions: ConditionElementListSyntax) {
conditions
.compactMap { $0.condition.as(OptionalBindingConditionSyntax.self)?.pattern }
.forEach { collectIdentifiers(fromPattern: $0) }
}
private func collectIdentifiers(fromPattern pattern: PatternSyntax) {
if let name = pattern.as(IdentifierPatternSyntax.self)?.identifier.text {
scope.addToCurrentScope(name)
}
}
}
private extension DeclaredIdentifiersTrackingVisitor.Scope {
mutating func addToCurrentScope(_ identifier: String) {
modifyLast { $0.insert(identifier) }
}
mutating func openChildScope() {
push([])
}
}

View File

@ -1,74 +0,0 @@
import SwiftSyntax
/// A SwiftSyntax `SyntaxVisitor` that produces absolute positions where violations should be reported.
open class ViolationsSyntaxVisitor: SyntaxVisitor {
/// Positions in a source file where violations should be reported.
public var violations: [ReasonedRuleViolation] = []
/// List of declaration types that shall be skipped while traversing the AST.
open var skippableDeclarations: [DeclSyntaxProtocol.Type] { [] }
override open func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == ActorDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == ClassDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == EnumDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == ExtensionDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == FunctionDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == FunctionDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == VariableDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == ProtocolDeclSyntax.self } ? .skipChildren : .visitChildren
}
override open func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
skippableDeclarations.contains { $0 == StructDeclSyntax.self } ? .skipChildren : .visitChildren
}
}
public extension Array where Element == DeclSyntaxProtocol.Type {
/// All visitable declaration syntax types.
static let all: Self = [
ActorDeclSyntax.self,
ClassDeclSyntax.self,
EnumDeclSyntax.self,
FunctionDeclSyntax.self,
ExtensionDeclSyntax.self,
ProtocolDeclSyntax.self,
StructDeclSyntax.self,
VariableDeclSyntax.self
]
/// Useful for class-specific checks since extensions and protocols do not allow nested classes.
static let extensionsAndProtocols: Self = [
ExtensionDeclSyntax.self,
ProtocolDeclSyntax.self
]
/// All declarations except for the specified ones.
///
/// - parameter declarations: The declarations to exclude from all declarations.
///
/// - returns: All declarations except for the specified ones.
static func allExcept(_ declarations: Element...) -> Self {
all.filter { decl in !declarations.contains { $0 == decl } }
}
}

View File

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

View File

@ -34,40 +34,31 @@ public struct RuleListDocumentation {
private var indexContents: String {
let defaultRuleDocumentations = ruleDocumentations.filter { !$0.isOptInRule }
let optInRuleDocumentations = ruleDocumentations.filter { $0.isOptInRule && !$0.isAnalyzerRule }
let analyzerRuleDocumentations = ruleDocumentations.filter { $0.isAnalyzerRule }
let optInRuleDocumentations = ruleDocumentations.filter { $0.isOptInRule }
return """
# Rule Directory
## Default Rules
\(defaultRuleDocumentations.map(makeListEntry).joined(separator: "\n"))
\(defaultRuleDocumentations
.map { "* `\($0.ruleIdentifier)`: \($0.ruleName)" }
.joined(separator: "\n"))
## Opt-in Rules
## Opt-In Rules
\(optInRuleDocumentations.map(makeListEntry).joined(separator: "\n"))
## Analyzer Rules
\(analyzerRuleDocumentations.map(makeListEntry).joined(separator: "\n"))
\(optInRuleDocumentations
.map { "* `\($0.ruleIdentifier)`: \($0.ruleName)" }
.joined(separator: "\n"))
"""
}
private func makeListEntry(from rule: RuleDocumentation) -> String {
"* [`\(rule.ruleIdentifier)`](\(rule.ruleIdentifier).md): \(rule.ruleName)"
}
private var swiftSyntaxDashboardContents: String {
let linterRuleDocumentations = ruleDocumentations.filter(\.isLinterRule)
let rulesUsingSourceKit = linterRuleDocumentations.filter(\.usesSourceKit)
let rulesNotUsingSourceKit = linterRuleDocumentations.filter { !$0.usesSourceKit }
let percentUsingSourceKit = Int(rulesUsingSourceKit.count * 100 / linterRuleDocumentations.count)
let enabledSourceKitRules = rulesUsingSourceKit.filter(\.isEnabledByDefault)
let disabledSourceKitRules = rulesUsingSourceKit.filter(\.isDisabledByDefault)
let enabledSourceKitFreeRules = rulesNotUsingSourceKit.filter(\.isEnabledByDefault)
let disabledSourceKitFreeRules = rulesNotUsingSourceKit.filter(\.isDisabledByDefault)
return """
# Swift Syntax Dashboard
@ -82,23 +73,35 @@ public struct RuleListDocumentation {
## Rules Using SourceKit
### Default Rules (\(enabledSourceKitRules.count))
### Enabled By Default (\(rulesUsingSourceKit.filter(\.isEnabledByDefault).count))
\(enabledSourceKitRules.map(makeListEntry).joined(separator: "\n"))
\(rulesUsingSourceKit
.filter(\.isEnabledByDefault)
.map { "* `\($0.ruleIdentifier)`: \($0.ruleName)" }
.joined(separator: "\n"))
### Opt-in Rules (\(disabledSourceKitRules.count))
### Opt-In (\(rulesUsingSourceKit.filter(\.isDisabledByDefault).count))
\(disabledSourceKitRules.map(makeListEntry).joined(separator: "\n"))
\(rulesUsingSourceKit
.filter(\.isDisabledByDefault)
.map { "* `\($0.ruleIdentifier)`: \($0.ruleName)" }
.joined(separator: "\n"))
## Rules not Using SourceKit
## Rules Not Using SourceKit
### Default Rules (\(enabledSourceKitFreeRules.count))
### Enabled By Default (\(rulesNotUsingSourceKit.filter(\.isEnabledByDefault).count))
\(enabledSourceKitFreeRules.map(makeListEntry).joined(separator: "\n"))
\(rulesNotUsingSourceKit
.filter(\.isEnabledByDefault)
.map { "* `\($0.ruleIdentifier)`: \($0.ruleName)" }
.joined(separator: "\n"))
### Opt-in Rules (\(disabledSourceKitFreeRules.count))
### Opt-In (\(rulesNotUsingSourceKit.filter(\.isDisabledByDefault).count))
\(disabledSourceKitFreeRules.map(makeListEntry).joined(separator: "\n"))
\(rulesNotUsingSourceKit
.filter(\.isDisabledByDefault)
.map { "* `\($0.ruleIdentifier)`: \($0.ruleName)" }
.joined(separator: "\n"))
"""
}

View File

@ -1,14 +0,0 @@
@_exported import SwiftLintBuiltInRules
@_exported import SwiftLintCore
import SwiftLintExtraRules
private let _registerAllRulesOnceImpl: Void = {
RuleRegistry.shared.register(rules: builtInRules + coreRules + extraRules())
}()
public extension RuleRegistry {
/// Register all rules. Should only be called once before any SwiftLint code is executed.
static func registerAllRulesOnce() {
_ = _registerAllRulesOnceImpl
}
}

View File

@ -1,6 +1,6 @@
import Dispatch
public extension Array where Element: Equatable {
extension Array where Element: Equatable {
/// The elements in this array, discarding duplicates after the first one.
/// Order-preserving.
var unique: [Element] {
@ -12,7 +12,7 @@ public extension Array where Element: Equatable {
}
}
public extension Array where Element: Hashable {
extension Array where Element: Hashable {
/// Produces an array containing the passed `obj` value.
/// If `obj` is an array already, return it.
/// If `obj` is a set, copy its elements to a new array.
@ -33,7 +33,7 @@ public extension Array where Element: Hashable {
}
}
public extension Array {
extension Array {
/// Produces an array containing the passed `obj` value.
/// If `obj` is an array already, return it.
/// If `obj` is a value of type `Element`, return a single-item array containing it.
@ -108,7 +108,7 @@ public extension Array {
}
}
public extension Collection {
extension Collection {
/// Whether this collection has one or more element.
var isNotEmpty: Bool {
return !isEmpty

View File

@ -1,7 +1,7 @@
import SourceKittenFramework
import SwiftSyntax
public extension ByteCount {
extension ByteCount {
/// Converts a SwiftSyntax `AbsolutePosition` to a SourceKitten `ByteCount`.
///
/// - parameter position: The SwiftSyntax position to convert.

View File

@ -1,4 +1,3 @@
// swiftlint:disable:next blanket_disable_command
// swiftlint:disable all
// Copied from https://github.com/apple/swift-algorithms/blob/main/Sources/Algorithms/Windows.swift

View File

@ -47,15 +47,15 @@ extension Configuration {
Self.nestedConfigIsSelfByIdentifierLock.unlock()
}
internal static func getIsNestedConfigurationSelf(forIdentifier identifier: String) -> Bool {
internal static func getIsNestedConfigurationSelf(forIdentifier identifier: String) -> Bool? {
Self.nestedConfigIsSelfByIdentifierLock.lock()
defer { Self.nestedConfigIsSelfByIdentifierLock.unlock() }
return Self.nestedConfigIsSelfByIdentifier[identifier] ?? false
return Self.nestedConfigIsSelfByIdentifier[identifier]
}
// MARK: SwiftLint Cache (On-Disk)
internal var cacheDescription: String {
if let computedCacheDescription {
if let computedCacheDescription = computedCacheDescription {
return computedCacheDescription
}
@ -92,7 +92,7 @@ extension Configuration {
do {
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil)
} catch {
Issue.genericWarning("Cannot create cache: " + error.localizedDescription).print()
queuedPrintError("Error while creating cache: " + error.localizedDescription)
}
return folder

View File

@ -1,7 +1,6 @@
import Foundation
@_spi(TestHelper)
public extension Configuration {
internal extension Configuration {
struct FileGraph: Hashable {
// MARK: - Properties
private static let defaultRemoteConfigTimeout: TimeInterval = 2
@ -11,7 +10,7 @@ public extension Configuration {
private let ignoreParentAndChildConfigs: Bool
private var vertices: Set<Vertex>
private var vertices: Set<Vertix>
private var edges: Set<Edge>
private var isBuilt = false
@ -19,7 +18,7 @@ public extension Configuration {
// MARK: - Initializers
internal init(commandLineChildConfigs: [String], rootDirectory: String, ignoreParentAndChildConfigs: Bool) {
let verticesArray = commandLineChildConfigs.map { config in
Vertex(string: config, rootDirectory: rootDirectory, isInitialVertex: true)
Vertix(string: config, rootDirectory: rootDirectory, isInitialVertix: true)
}
vertices = Set(verticesArray)
edges = Set(zip(verticesArray, verticesArray.dropFirst()).map { Edge(parent: $0.0, child: $0.1) })
@ -28,6 +27,14 @@ public extension Configuration {
self.ignoreParentAndChildConfigs = ignoreParentAndChildConfigs
}
internal init(config: String, rootDirectory: String, ignoreParentAndChildConfigs: Bool) throws {
self.init(
commandLineChildConfigs: [config],
rootDirectory: rootDirectory,
ignoreParentAndChildConfigs: ignoreParentAndChildConfigs
)
}
/// Dummy init to get a FileGraph that just represents a root directory
internal init(rootDirectory: String) {
self.init(
@ -56,11 +63,11 @@ public extension Configuration {
)
}
internal func includesFile(atPath path: String) -> Bool {
guard isBuilt else { return false }
internal func includesFile(atPath path: String) -> Bool? {
guard isBuilt else { return nil }
return vertices.contains { vertex in
if case let .existing(filePath) = vertex.filePath {
return vertices.contains { vertix in
if case let .existing(filePath) = vertix.filePath {
return path == filePath
}
@ -70,19 +77,19 @@ public extension Configuration {
// MARK: Building
private mutating func build() throws {
for vertex in vertices {
try process(vertex: vertex)
for vertix in vertices {
try process(vertix: vertix)
}
isBuilt = true
}
private mutating func process(
vertex: Vertex,
vertix: Vertix,
remoteConfigTimeoutOverride: TimeInterval? = nil,
remoteConfigTimeoutIfCachedOverride: TimeInterval? = nil
) throws {
try vertex.build(
try vertix.build(
remoteConfigTimeout: remoteConfigTimeoutOverride ?? Configuration.FileGraph.defaultRemoteConfigTimeout,
remoteConfigTimeoutIfCached: remoteConfigTimeoutIfCachedOverride
?? remoteConfigTimeoutOverride ?? Configuration.FileGraph.defaultRemoteConfigTimeoutIfCached
@ -91,13 +98,13 @@ public extension Configuration {
if !ignoreParentAndChildConfigs {
try processPossibleReference(
ofType: .childConfig,
from: vertex,
from: vertix,
remoteConfigTimeoutOverride: remoteConfigTimeoutOverride,
remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCachedOverride
)
try processPossibleReference(
ofType: .parentConfig,
from: vertex,
from: vertix,
remoteConfigTimeoutOverride: remoteConfigTimeoutOverride,
remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCachedOverride
)
@ -106,46 +113,46 @@ public extension Configuration {
private mutating func processPossibleReference(
ofType type: EdgeType,
from vertex: Vertex,
from vertix: Vertix,
remoteConfigTimeoutOverride: TimeInterval?,
remoteConfigTimeoutIfCachedOverride: TimeInterval?
) throws {
let key = type == .childConfig ? Configuration.Key.childConfig.rawValue
: Configuration.Key.parentConfig.rawValue
if let reference = vertex.configurationDict[key] as? String {
let referencedVertex = Vertex(string: reference, rootDirectory: vertex.rootDirectory,
isInitialVertex: false)
if let reference = vertix.configurationDict[key] as? String {
let referencedVertix = Vertix(string: reference, rootDirectory: vertix.rootDirectory,
isInitialVertix: false)
// Local vertices are allowed to have local / remote references
// Remote vertices are only allowed to have remote references
if vertex.originatesFromRemote && !referencedVertex.originatesFromRemote {
throw Issue.genericWarning("Remote configs are not allowed to reference local configs.")
if vertix.originatesFromRemote && !referencedVertix.originatesFromRemote {
throw ConfigurationError.generic("Remote configs are not allowed to reference local configs.")
} else {
let existingVertex = findPossiblyExistingVertex(sameAs: referencedVertex)
let existingVertexCopy = existingVertex.map { $0.copy(withNewRootDirectory: rootDirectory) }
let existingVertix = findPossiblyExistingVertix(sameAs: referencedVertix)
let existingVertixCopy = existingVertix.map { $0.copy(withNewRootDirectory: rootDirectory) }
edges.insert(
type == .childConfig
? Edge(parent: vertex, child: existingVertexCopy ?? referencedVertex)
: Edge(parent: existingVertexCopy ?? referencedVertex, child: vertex)
? Edge(parent: vertix, child: existingVertixCopy ?? referencedVertix)
: Edge(parent: existingVertixCopy ?? referencedVertix, child: vertix)
)
if existingVertex == nil {
vertices.insert(referencedVertex)
if existingVertix == nil {
vertices.insert(referencedVertix)
// Use timeout config from vertex / parent of vertex if some
// Use timeout config from vertix / parent of vertix if some
let remoteConfigTimeout =
vertex.configurationDict[Configuration.Key.remoteConfigTimeout.rawValue]
vertix.configurationDict[Configuration.Key.remoteConfigTimeout.rawValue]
as? TimeInterval
?? remoteConfigTimeoutOverride // from vertex parent
?? remoteConfigTimeoutOverride // from vertix parent
let remoteConfigTimeoutIfCached =
vertex.configurationDict[Configuration.Key.remoteConfigTimeoutIfCached.rawValue]
vertix.configurationDict[Configuration.Key.remoteConfigTimeoutIfCached.rawValue]
as? TimeInterval
?? remoteConfigTimeoutIfCachedOverride // from vertex parent
?? remoteConfigTimeoutIfCachedOverride // from vertix parent
try process(
vertex: referencedVertex,
vertix: referencedVertix,
remoteConfigTimeoutOverride: remoteConfigTimeout,
remoteConfigTimeoutIfCachedOverride: remoteConfigTimeoutIfCached
)
@ -154,10 +161,10 @@ public extension Configuration {
}
}
private func findPossiblyExistingVertex(sameAs vertex: Vertex) -> Vertex? {
private func findPossiblyExistingVertix(sameAs vertix: Vertix) -> Vertix? {
return vertices.first {
$0.originalRemoteString != nil && $0.originalRemoteString == vertex.originalRemoteString
} ?? vertices.first { $0.filePath == vertex.filePath }
$0.originalRemoteString != nil && $0.originalRemoteString == vertix.originalRemoteString
} ?? vertices.first { $0.filePath == vertix.filePath }
}
// MARK: Validating
@ -165,11 +172,11 @@ public extension Configuration {
/// If successful, returns array of configuration dicts that represents the graph
private func validate() throws -> [(configurationDict: [String: Any], rootDirectory: String)] {
// Detect cycles via back-edge detection during DFS
func walkDown(stack: [Vertex]) throws {
func walkDown(stack: [Vertix]) throws {
// Please note that the equality check (`==`), not the identity check (`===`) is used
let children = edges.filter { $0.parent == stack.last }.map { $0.child! }
if stack.contains(where: children.contains) {
throw Issue.genericWarning("There's a cycle of child / parent config references. "
throw ConfigurationError.generic("There's a cycle of child / parent config references. "
+ "Please check the hierarchy of configuration files passed via the command line "
+ "and the childConfig / parentConfig entries within them.")
}
@ -180,36 +187,36 @@ public extension Configuration {
// Detect ambiguities
if (edges.contains { edge in edges.filter { $0.parent == edge.parent }.count > 1 }) {
throw Issue.genericWarning("There's an ambiguity in the child / parent configuration tree: "
throw ConfigurationError.generic("There's an ambiguity in the child / parent configuration tree: "
+ "More than one parent is declared for a specific configuration, "
+ "where there should only be exactly one.")
}
if (edges.contains { edge in edges.filter { $0.child == edge.child }.count > 1 }) {
throw Issue.genericWarning("There's an ambiguity in the child / parent configuration tree: "
throw ConfigurationError.generic("There's an ambiguity in the child / parent configuration tree: "
+ "More than one child is declared for a specific configuration, "
+ "where there should only be exactly one.")
}
// The graph should be like an array if validation passed -> return that array
guard
let startingVertex = (vertices.first { vertex in !edges.contains { $0.child == vertex } })
let startingVertix = (vertices.first { vertix in !edges.contains { $0.child == vertix } })
else {
guard vertices.isEmpty else {
throw Issue.genericWarning("Unknown Configuration Error")
throw ConfigurationError.generic("Unknown Configuration Error")
}
return []
}
var verticesToMerge = [startingVertex]
while let vertex = (edges.first { $0.parent == verticesToMerge.last }?.child) {
guard !verticesToMerge.contains(vertex) else {
var verticesToMerge = [startingVertix]
while let vertix = (edges.first { $0.parent == verticesToMerge.last }?.child) {
guard !verticesToMerge.contains(vertix) else {
// This shouldn't happen on a cycle free graph but let's safeguard
throw Issue.genericWarning("Unknown Configuration Error")
throw ConfigurationError.generic("Unknown Configuration Error")
}
verticesToMerge.append(vertex)
verticesToMerge.append(vertix)
}
return verticesToMerge.map {

View File

@ -7,8 +7,8 @@ internal extension Configuration.FileGraph {
case existing(path: String)
}
// MARK: - Vertex
class Vertex: Hashable {
// MARK: - Vertix
class Vertix: Hashable {
internal let originalRemoteString: String?
internal var originatesFromRemote: Bool { originalRemoteString != nil }
internal var rootDirectory: String {
@ -22,11 +22,11 @@ internal extension Configuration.FileGraph {
}
private let originalRootDirectory: String
let isInitialVertex: Bool
let isInitialVertix: Bool
private(set) var filePath: FilePath
private(set) var configurationDict: [String: Any] = [:]
init(string: String, rootDirectory: String, isInitialVertex: Bool) {
init(string: String, rootDirectory: String, isInitialVertix: Bool) {
originalRootDirectory = rootDirectory
if string.hasPrefix("http://") || string.hasPrefix("https://") {
originalRemoteString = string
@ -37,25 +37,25 @@ internal extension Configuration.FileGraph {
path: string.bridge().absolutePathRepresentation(rootDirectory: rootDirectory)
)
}
self.isInitialVertex = isInitialVertex
self.isInitialVertix = isInitialVertix
}
init(originalRemoteString: String?, originalRootDirectory: String, filePath: FilePath, isInitialVertex: Bool) {
init(originalRemoteString: String?, originalRootDirectory: String, filePath: FilePath, isInitialVertix: Bool) {
self.originalRemoteString = originalRemoteString
self.originalRootDirectory = originalRootDirectory
self.filePath = filePath
self.isInitialVertex = isInitialVertex
self.isInitialVertix = isInitialVertix
}
internal func copy(withNewRootDirectory rootDirectory: String) -> Vertex {
let vertex = Vertex(
internal func copy(withNewRootDirectory rootDirectory: String) -> Vertix {
let vertix = Vertix(
originalRemoteString: originalRemoteString,
originalRootDirectory: rootDirectory,
filePath: filePath,
isInitialVertex: isInitialVertex
isInitialVertix: isInitialVertix
)
vertex.configurationDict = configurationDict
return vertex
vertix.configurationDict = configurationDict
return vertix
}
internal func build(
@ -73,15 +73,15 @@ internal extension Configuration.FileGraph {
private func read(at path: String) throws -> String {
guard !path.isEmpty && FileManager.default.fileExists(atPath: path) else {
throw isInitialVertex ?
Issue.initialFileNotFound(path: path) :
Issue.genericWarning("File \(path) can't be found.")
throw isInitialVertix ?
ConfigurationError.initialFileNotFound(path: path) :
ConfigurationError.generic("File \(path) can't be found.")
}
return try String(contentsOfFile: path, encoding: .utf8)
}
internal static func == (lhs: Vertex, rhs: Vertex) -> Bool {
internal static func == (lhs: Vertix, rhs: Vertix) -> Bool {
return lhs.filePath == rhs.filePath
&& lhs.originalRemoteString == rhs.originalRemoteString
&& lhs.rootDirectory == rhs.rootDirectory
@ -96,8 +96,8 @@ internal extension Configuration.FileGraph {
// MARK: - Edge
struct Edge: Hashable {
var parent: Vertex!
var child: Vertex!
var parent: Vertix!
var child: Vertix!
}
// MARK: - EdgeType

View File

@ -7,8 +7,7 @@ public extension Configuration {
case spaces(count: Int)
/// The default indentation style if none is explicitly provided.
@_spi(TestHelper)
public static var `default` = spaces(count: 4)
static var `default` = spaces(count: 4)
/// Creates an indentation style based on an untyped configuration value.
///

View File

@ -1,11 +1,6 @@
import Foundation
extension Configuration {
public enum ExcludeBy {
case prefix
case paths(excludedPaths: [String])
}
// MARK: Lintable Paths
/// Returns the files that can be linted by SwiftLint in the specified parent path.
///
@ -17,8 +12,8 @@ extension Configuration {
///
/// - returns: Files to lint.
public func lintableFiles(inPath path: String, forceExclude: Bool,
excludeBy: ExcludeBy) -> [SwiftLintFile] {
return lintablePaths(inPath: path, forceExclude: forceExclude, excludeBy: excludeBy)
excludeByPrefix: Bool = false) -> [SwiftLintFile] {
return lintablePaths(inPath: path, forceExclude: forceExclude, excludeByPrefix: excludeByPrefix)
.compactMap(SwiftLintFile.init(pathDeferringReading:))
}
@ -35,33 +30,20 @@ extension Configuration {
internal func lintablePaths(
inPath path: String,
forceExclude: Bool,
excludeBy: ExcludeBy,
excludeByPrefix: Bool = false,
fileManager: LintableFileManager = FileManager.default
) -> [String] {
if fileManager.isFile(atPath: path) {
if forceExclude {
switch excludeBy {
case .prefix:
return filterExcludedPathsByPrefix(in: [path.absolutePathStandardized()])
case .paths(let excludedPaths):
return filterExcludedPaths(excludedPaths, in: [path.absolutePathStandardized()])
}
}
// If path is a file and we're not forcing excludes, skip filtering with excluded/included paths
return [path]
}
// If path is a file and we're not forcing excludes, skip filtering with excluded/included paths
if path.isFile && !forceExclude { return [path] }
let pathsForPath = includedPaths.isEmpty ? fileManager.filesToLint(inPath: path, rootDirectory: nil) : []
let includedPaths = self.includedPaths
.flatMap(Glob.resolveGlob)
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }
switch excludeBy {
case .prefix:
return filterExcludedPathsByPrefix(in: pathsForPath, includedPaths)
case .paths(let excludedPaths):
return filterExcludedPaths(excludedPaths, in: pathsForPath, includedPaths)
}
return excludeByPrefix
? filterExcludedPathsByPrefix(in: pathsForPath, includedPaths)
: filterExcludedPaths(fileManager: fileManager, in: pathsForPath, includedPaths)
}
/// Returns an array of file paths after removing the excluded paths as defined by this configuration.
@ -71,7 +53,7 @@ extension Configuration {
///
/// - returns: The input paths after removing the excluded paths.
public func filterExcludedPaths(
_ excludedPaths: [String],
fileManager: LintableFileManager = FileManager.default,
in paths: [String]...
) -> [String] {
let allPaths = paths.flatMap { $0 }
@ -82,6 +64,7 @@ extension Configuration {
let result = NSMutableOrderedSet(array: allPaths)
#endif
let excludedPaths = self.excludedPaths(fileManager: fileManager)
result.minusSet(Set(excludedPaths))
// swiftlint:disable:next force_cast
return result.map { $0 as! String }
@ -108,7 +91,7 @@ extension Configuration {
/// - parameter fileManager: The file manager to get child paths in a given parent location.
///
/// - returns: The expanded excluded file paths.
public func excludedPaths(fileManager: LintableFileManager = FileManager.default) -> [String] {
private func excludedPaths(fileManager: LintableFileManager) -> [String] {
return excludedPaths
.flatMap(Glob.resolveGlob)
.parallelFlatMap { fileManager.filesToLint(inPath: $0, rootDirectory: rootDirectory) }

View File

@ -6,8 +6,7 @@ import SourceKittenFramework
extension Configuration {
// MARK: - Methods: Merging
@_spi(TestHelper)
public func merged(
internal func merged(
withChild childConfiguration: Configuration,
rootDirectory: String
) -> Configuration {
@ -106,7 +105,7 @@ extension Configuration {
config = self
} else if
FileManager.default.fileExists(atPath: configurationSearchPath),
!fileGraph.includesFile(atPath: configurationSearchPath)
fileGraph.includesFile(atPath: configurationSearchPath) != true
{
// Use self merged with the nested config that was found
// iff that nested config has not already been used to build the main config

View File

@ -1,3 +1,5 @@
// swiftlint:disable inclusive_language - To ease a migration from the previous `whitelist_rules`
extension Configuration {
// MARK: - Subtypes
internal enum Key: String, CaseIterable {
@ -9,8 +11,10 @@ extension Configuration {
case optInRules = "opt_in_rules"
case reporter = "reporter"
case swiftlintVersion = "swiftlint_version"
case useNestedConfigs = "use_nested_configs" // deprecated, always enabled
case warningThreshold = "warning_threshold"
case onlyRules = "only_rules"
case whitelistRules = "whitelist_rules" // deprecated in favor of onlyRules
case indentation = "indentation"
case analyzerRules = "analyzer_rules"
case allowZeroLintableFiles = "allow_zero_lintable_files"
@ -34,7 +38,7 @@ extension Configuration {
/// - parameter cachePath: The location of the persisted cache on disk.
public init(
dict: [String: Any],
ruleList: RuleList = RuleRegistry.shared.list,
ruleList: RuleList = primaryRuleList,
enableAllRules: Bool = false,
cachePath: String? = nil
) throws {
@ -44,7 +48,8 @@ extension Configuration {
let optInRules = defaultStringArray(dict[Key.optInRules.rawValue] ?? dict[Key.enabledRules.rawValue])
let disabledRules = defaultStringArray(dict[Key.disabledRules.rawValue])
let onlyRules = defaultStringArray(dict[Key.onlyRules.rawValue])
// Use either the new 'only_rules' or fallback to the deprecated 'whitelist_rules'
let onlyRules = defaultStringArray(dict[Key.onlyRules.rawValue] ?? dict[Key.whitelistRules.rawValue])
let analyzerRules = defaultStringArray(dict[Key.analyzerRules.rawValue])
Self.warnAboutInvalidKeys(configurationDictionary: dict, ruleList: ruleList)
@ -52,7 +57,6 @@ extension Configuration {
configurationDictionary: dict, disabledRules: disabledRules,
optInRules: optInRules, onlyRules: onlyRules, ruleList: ruleList
)
Self.warnAboutMisplacedAnalyzerRules(optInRules: optInRules, ruleList: ruleList)
let allRulesWrapped: [ConfigurationRuleWrapper]
do {
@ -60,7 +64,7 @@ extension Configuration {
} catch let RuleListError.duplicatedConfigurations(ruleType) {
let aliases = ruleType.description.deprecatedAliases.map { "'\($0)'" }.joined(separator: ", ")
let identifier = ruleType.description.identifier
throw Issue.genericWarning(
throw ConfigurationError.generic(
"Multiple configurations found for '\(identifier)'. Check for any aliases: \(aliases)."
)
}
@ -102,7 +106,8 @@ extension Configuration {
if let indentationStyle = Self.IndentationStyle(rawIndentation) {
return indentationStyle
}
Issue.invalidConfiguration(ruleID: Key.indentation.rawValue).print()
queuedPrintError("Invalid configuration for '\(Key.indentation)'. Falling back to default.")
return .default
}
@ -118,7 +123,23 @@ extension Configuration {
) {
// Deprecation warning for "enabled_rules"
if dict[Key.enabledRules.rawValue] != nil {
Issue.renamedIdentifier(old: Key.enabledRules.rawValue, new: Key.optInRules.rawValue).print()
queuedPrintError("warning: '\(Key.enabledRules.rawValue)' has been renamed to " +
"'\(Key.optInRules.rawValue)' and will be completely removed in a " +
"future release.")
}
// Deprecation warning for "use_nested_configs"
if dict[Key.useNestedConfigs.rawValue] != nil {
queuedPrintError("warning: Support for '\(Key.useNestedConfigs.rawValue)' has " +
"been deprecated and its value is now ignored. Nested configuration files are " +
"now always considered.")
}
// Deprecation warning for "whitelist_rules"
if dict[Key.whitelistRules.rawValue] != nil {
queuedPrintError("'\(Key.whitelistRules.rawValue)' has been renamed to " +
"'\(Key.onlyRules.rawValue)' and will be completely removed in a " +
"future release.")
}
// Deprecation warning for rules
@ -132,7 +153,10 @@ extension Configuration {
}
for (deprecatedIdentifier, identifier) in deprecatedUsages {
Issue.renamedIdentifier(old: deprecatedIdentifier, new: identifier).print()
queuedPrintError(
"warning: '\(deprecatedIdentifier)' rule has been renamed to '\(identifier)' and will be "
+ "completely removed in a future release."
)
}
}
@ -140,7 +164,7 @@ extension Configuration {
// Log an error when supplying invalid keys in the configuration dictionary
let invalidKeys = Set(dict.keys).subtracting(validKeys(ruleList: ruleList))
if invalidKeys.isNotEmpty {
Issue.invalidConfigurationKeys(invalidKeys.sorted()).print()
queuedPrintError("warning: Configuration contains invalid keys:\n\(invalidKeys)")
}
}
@ -155,7 +179,7 @@ extension Configuration {
continue
}
let message = "Found a configuration for '\(identifier)' rule"
let message = "warning: Found a configuration for '\(identifier)' rule"
switch rulesMode {
case .allEnabled:
@ -163,32 +187,19 @@ extension Configuration {
case .only(let onlyRules):
if Set(onlyRules).isDisjoint(with: rule.description.allIdentifiers) {
Issue.genericWarning("\(message), but it is not present on '\(Key.onlyRules.rawValue)'.").print()
queuedPrintError("\(message), but it is not present on " +
"'\(Key.onlyRules.rawValue)'.")
}
case let .default(disabled: disabledRules, optIn: optInRules):
if rule is OptInRule.Type, Set(optInRules).isDisjoint(with: rule.description.allIdentifiers) {
Issue.genericWarning("\(message), but it is not enabled on '\(Key.optInRules.rawValue)'.").print()
queuedPrintError("\(message), but it is not enabled on " +
"'\(Key.optInRules.rawValue)'.")
} else if Set(disabledRules).isSuperset(of: rule.description.allIdentifiers) {
Issue.genericWarning("\(message), but it is disabled on '\(Key.disabledRules.rawValue)'.").print()
queuedPrintError("\(message), but it is disabled on " +
"'\(Key.disabledRules.rawValue)'.")
}
}
}
}
private static func warnAboutMisplacedAnalyzerRules(optInRules: [String], ruleList: RuleList) {
let analyzerRules = ruleList.list
.filter { $0.value.self is AnalyzerRule.Type }
.map(\.key)
Set(analyzerRules).intersection(optInRules)
.sorted()
.forEach {
Issue.genericWarning(
"""
'\($0)' should be listed in the 'analyzer_rules' configuration section \
for more clarity as it is only run by 'swiftlint analyze'.
"""
).print()
}
}
}

View File

@ -65,7 +65,7 @@ internal extension Configuration.FileGraph.FilePath {
// Handle wrong url format
guard let url = URL(string: urlString) else {
throw Issue.genericWarning("Invalid configuration entry: \"\(urlString)\" isn't a valid url.")
throw ConfigurationError.generic("Invalid configuration entry: \"\(urlString)\" isn't a valid url.")
}
// Load from url
@ -118,7 +118,7 @@ internal extension Configuration.FileGraph.FilePath {
}
private mutating func handleMissingNetwork(urlString: String, cachedFilePath: String?) throws -> String {
if let cachedFilePath {
if let cachedFilePath = cachedFilePath {
queuedPrintError(
"warning: No internet connectivity: Unable to load remote config from \"\(urlString)\". "
+ "Using cached version as a fallback."
@ -126,7 +126,7 @@ internal extension Configuration.FileGraph.FilePath {
self = .existing(path: cachedFilePath)
return cachedFilePath
} else {
throw Issue.genericWarning(
throw ConfigurationError.generic(
"No internet connectivity: Unable to load remote config from \"\(urlString)\". "
+ "Also didn't found cached version to fallback to."
)
@ -139,7 +139,7 @@ internal extension Configuration.FileGraph.FilePath {
taskDone: Bool,
timeout: TimeInterval
) throws -> String {
if let cachedFilePath {
if let cachedFilePath = cachedFilePath {
if taskDone {
queuedPrintError(
"warning: Unable to load remote config from \"\(urlString)\". Using cached version as a fallback."
@ -155,12 +155,12 @@ internal extension Configuration.FileGraph.FilePath {
return cachedFilePath
} else {
if taskDone {
throw Issue.genericWarning(
throw ConfigurationError.generic(
"Unable to load remote config from \"\(urlString)\". "
+ "Also didn't found cached version to fallback to."
)
} else {
throw Issue.genericWarning(
throw ConfigurationError.generic(
"Timeout (\(timeout) sec): Unable to load remote config from \"\(urlString)\". "
+ "Also didn't found cached version to fallback to."
)
@ -169,14 +169,12 @@ internal extension Configuration.FileGraph.FilePath {
}
private mutating func handleFileWriteFailure(urlString: String, cachedFilePath: String?) throws -> String {
if let cachedFilePath {
queuedPrintError(
"warning: Unable to cache remote config from \"\(urlString)\". Using cached version as a fallback."
)
if let cachedFilePath = cachedFilePath {
queuedPrintError("Unable to cache remote config from \"\(urlString)\". Using cached version as a fallback.")
self = .existing(path: cachedFilePath)
return cachedFilePath
} else {
throw Issue.genericWarning(
throw ConfigurationError.generic(
"Unable to cache remote config from \"\(urlString)\". Also didn't found cached version to fallback to."
)
}
@ -261,7 +259,7 @@ internal extension Configuration.FileGraph.FilePath {
contents: Data(newGitignoreAppendix.utf8),
attributes: [:]
) else {
throw Issue.genericWarning("Issue maintaining remote config cache.")
throw ConfigurationError.generic("Issue maintaining remote config cache.")
}
} else {
var contents = try String(contentsOfFile: Configuration.FileGraph.FilePath.gitignorePath, encoding: .utf8)

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