From 0969e1c1a79a83fbb1a9b4e7f2dc2598fca3056c Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Mon, 19 May 2025 07:58:18 +0200 Subject: [PATCH 01/80] Initial implementation of allowed_types option for `one-declaration-per-file` rule --- .../Idiomatic/OneDeclarationPerFileRule.swift | 29 ++++++++++++++++--- .../OneDeclarationPerFileConfiguration.swift | 26 +++++++++++++++++ .../default_rule_configurations.yml | 1 + 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index 784a4e3456..a220a4030a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -2,8 +2,7 @@ import SwiftSyntax @SwiftSyntaxRule(optIn: true) struct OneDeclarationPerFileRule: Rule { - var configuration = SeverityConfiguration(.warning) - + var configuration = OneDeclarationPerFileConfiguration() static let description = RuleDescription( identifier: "one_declaration_per_file", name: "One Declaration per File", @@ -22,6 +21,18 @@ struct OneDeclarationPerFileRule: Rule { struct N {} } """), + Example(""" + enum Foo { + } + struct Bar { + } + """, + configuration: ["allowed_types": ["enum", "struct"]]), + Example(""" + struct Foo {} + struct Bar {} + """, + configuration: ["allowed_types": ["struct"]]), ], triggeringExamples: [ Example(""" @@ -36,14 +47,24 @@ struct OneDeclarationPerFileRule: Rule { struct Foo {} ↓struct Bar {} """), + Example(""" + struct Foo {} + ↓enum Bar {} + """, + configuration: ["allowed_types": ["protocol"]]), ] ) } private extension OneDeclarationPerFileRule { final class Visitor: ViolationsSyntaxVisitor { + private let allowedTypes: Set private var declarationVisited = false override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + override init(configuration: OneDeclarationPerFileConfiguration, file: SwiftLintFile) { + allowedTypes = Set(configuration.enabledTypes.map(\.rawValue)) + super.init(configuration: configuration, file: file) + } override func visitPost(_ node: ActorDeclSyntax) { appendViolationIfNeeded(node: node.actorKeyword) @@ -66,10 +87,10 @@ private extension OneDeclarationPerFileRule { } func appendViolationIfNeeded(node: TokenSyntax) { - if declarationVisited { + defer { declarationVisited = true } + if declarationVisited && !allowedTypes.contains(node.text) { violations.append(node.positionAfterSkippingLeadingTrivia) } - declarationVisited = true } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift new file mode 100644 index 0000000000..93951a8d03 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift @@ -0,0 +1,26 @@ +import SwiftLintCore + +@AutoConfigParser +struct OneDeclarationPerFileConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = OneDeclarationPerFileRule + + @AcceptableByConfigurationElement + enum AllowedType: String, CaseIterable { + case `actor` + case `class` + case `enum` + case `protocol` + case `struct` + static let all = Set(allCases) + } + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + + @ConfigurationElement(key: "allowed_types") + private(set) var allowedTypes: [AllowedType] = [] + + var enabledTypes: Set { + Set(self.allowedTypes) + } +} diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 981d667680..bd35934653 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -788,6 +788,7 @@ one_declaration_per_file: meta: opt-in: true correctable: false + allowed_types: [] opening_brace: severity: warning ignore_multiline_type_headers: false From ea3c327baf789ebd9b6a009cfc1acce8f440189a Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Tue, 20 May 2025 21:11:48 +0200 Subject: [PATCH 02/80] Add unit test, add changelog entry --- CHANGELOG.md | 4 +- .../Idiomatic/OneDeclarationPerFileRule.swift | 1 + .../OneDeclarationPerFileConfiguration.swift | 1 + ...eDeclarationPerFileConfigurationTest.swift | 39 +++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b5a96b8b..00d47634e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ ### Enhancements -* None. +* Add new `allowed_types` option to the `one_declaration_per_file` rule. + [Alfons Hoogervorst](https://github.com/snofla) + [#6072](https://github.com/realm/SwiftLint/issues/6072) ### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index a220a4030a..8deffc644d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -61,6 +61,7 @@ private extension OneDeclarationPerFileRule { private let allowedTypes: Set private var declarationVisited = false override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + override init(configuration: OneDeclarationPerFileConfiguration, file: SwiftLintFile) { allowedTypes = Set(configuration.enabledTypes.map(\.rawValue)) super.init(configuration: configuration, file: file) diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift index 93951a8d03..b90df8fc2f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift @@ -11,6 +11,7 @@ struct OneDeclarationPerFileConfiguration: SeverityBasedRuleConfiguration { case `enum` case `protocol` case `struct` + static let all = Set(allCases) } diff --git a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift new file mode 100644 index 0000000000..89769decbb --- /dev/null +++ b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift @@ -0,0 +1,39 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { + func testOneDeclarationPerFileConfigurationCheckSettingAllowedTypes() throws { + let initial: [OneDeclarationPerFileConfiguration.AllowedType] = [ + .actor, .class + ] + let config = OneDeclarationPerFileConfiguration(severityConfiguration: .warning, allowedTypes: initial) + XCTAssertEqual(Set(initial), config.enabledTypes) + } + + func testOneDeclarationPerFileConfigurationGoodConfig() throws { + let allowedTypes = OneDeclarationPerFileConfiguration.AllowedType.all + let allowedTypesString: [String] = allowedTypes.map(\.rawValue) + .sorted() + let goodConfig: [String: Any] = [ + "severity": "error", + "allowed_types": allowedTypesString + ] + var configuration = OneDeclarationPerFileConfiguration() + try configuration.apply(configuration: goodConfig) + XCTAssertEqual(configuration.severityConfiguration.severity, .error) + XCTAssertEqual(configuration.enabledTypes, allowedTypes) + } + + func testOneDeclarationPerFileConfigurationBadConfigWrongTypes() throws { + let badConfig: [String: Any] = [ + "severity": "error", + "allowed_types": ["clas"] + ] + var configuration = OneDeclarationPerFileConfiguration() + checkError(Issue.invalidConfiguration(ruleID: OneDeclarationPerFileRule.identifier)) { + try configuration.apply(configuration: badConfig) + } + XCTAssert(configuration.enabledTypes == []) + } +} From dc0a57fbfa3e69aa5770cbf44502fbca5ce241ef Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Wed, 21 May 2025 05:31:54 +0200 Subject: [PATCH 03/80] Minor fixes --- .../OneDeclarationPerFileConfigurationTest.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift index 89769decbb..c02b8190d8 100644 --- a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift +++ b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift @@ -17,7 +17,7 @@ final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { .sorted() let goodConfig: [String: Any] = [ "severity": "error", - "allowed_types": allowedTypesString + "allowed_types": allowedTypesString, ] var configuration = OneDeclarationPerFileConfiguration() try configuration.apply(configuration: goodConfig) @@ -28,12 +28,11 @@ final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { func testOneDeclarationPerFileConfigurationBadConfigWrongTypes() throws { let badConfig: [String: Any] = [ "severity": "error", - "allowed_types": ["clas"] + "allowed_types": ["clas"], ] var configuration = OneDeclarationPerFileConfiguration() checkError(Issue.invalidConfiguration(ruleID: OneDeclarationPerFileRule.identifier)) { try configuration.apply(configuration: badConfig) } - XCTAssert(configuration.enabledTypes == []) - } + } } From f72f195ecea7eadee61a5822fe024b5a686753b9 Mon Sep 17 00:00:00 2001 From: Martin Redington Date: Wed, 21 May 2025 11:34:46 +0100 Subject: [PATCH 04/80] Fix error reporting (#6061) --- CHANGELOG.md | 5 ++++- Source/SwiftLintCore/Models/Issue.swift | 6 +++--- Source/SwiftLintFramework/Configuration/Configuration.swift | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b5a96b8b..8758375069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,10 @@ ### Bug Fixes -* None. +* Improved error reporting when SwiftLint exits, because of an invalid configuration file + or other error. + [Martin Redington](https://github.com/mildm8nnered) + [#6052](https://github.com/realm/SwiftLint/issues/6052) ## 0.59.1: Crisp Spring Clean diff --git a/Source/SwiftLintCore/Models/Issue.swift b/Source/SwiftLintCore/Models/Issue.swift index b16417b139..86e0190b34 100644 --- a/Source/SwiftLintCore/Models/Issue.swift +++ b/Source/SwiftLintCore/Models/Issue.swift @@ -116,7 +116,7 @@ public enum Issue: LocalizedError, Equatable { } /// The issues description which is ready to be printed to the console. - package var errorDescription: String { + public var errorDescription: String? { switch self { case .genericError: return "error: \(message)" @@ -132,8 +132,8 @@ public enum Issue: LocalizedError, Equatable { if case .ruleDeprecated = self, !Self.printDeprecationWarnings { return } - Self.printQueueContinuation?.yield(errorDescription) - queuedPrintError(errorDescription) + Self.printQueueContinuation?.yield(localizedDescription) + queuedPrintError(localizedDescription) } private var message: String { diff --git a/Source/SwiftLintFramework/Configuration/Configuration.swift b/Source/SwiftLintFramework/Configuration/Configuration.swift index bf5d6d9bc1..ee3e2fa788 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration.swift @@ -280,7 +280,7 @@ public struct Configuration { if useDefaultConfigOnFailure ?? !hasCustomConfigurationFiles { // No files were explicitly specified, so maybe the user doesn't want a config at all -> warn queuedPrintError( - "\(Issue.wrap(error: error).errorDescription) – Falling back to default configuration" + "\(Issue.wrap(error: error).localizedDescription) – Falling back to default configuration" ) self.init(rulesMode: rulesMode, cachePath: cachePath) } else { From a3aec89e21f239a274be3c984a473b69789334b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 21 May 2025 12:29:00 +0100 Subject: [PATCH 05/80] Add test case for trailing comma in multiline arguments (#6085) --- .../Rules/Style/MultilineArgumentsRuleExamples.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift index da7c1a3f0e..20e4124d41 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineArgumentsRuleExamples.swift @@ -69,6 +69,12 @@ internal struct MultilineArgumentsRuleExamples { print("b") } """), + Example(""" + f( + foo: 1, + bar: false, + ) + """), ] static let triggeringExamples = [ From 0338af7945c19046f254b5b67d0c48893860ac3e Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Wed, 21 May 2025 19:22:35 +0200 Subject: [PATCH 06/80] Fix default rules --- Tests/IntegrationTests/default_rule_configurations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index bd35934653..d7b44ff91f 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -785,10 +785,10 @@ object_literal: correctable: false one_declaration_per_file: severity: warning + allowed_types: [] meta: opt-in: true correctable: false - allowed_types: [] opening_brace: severity: warning ignore_multiline_type_headers: false From 9a54239e4895ac89baa0a8530dacdd49391aa65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 15 Jun 2025 18:50:49 +0200 Subject: [PATCH 07/80] Run tests on Xcode 16.4 Xcode 15.4 has been removed from macOS 15 runners. --- azure-pipelines.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index fd284170b3..77b8b5c14d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -37,12 +37,9 @@ jobs: image: 'macOS-14' xcode: '15.4' # '14 : Xcode 16.3': Runs on Buildkite. - '15 : Xcode 15.4': + '15 : Xcode 16.4': image: 'macOS-15' - xcode: '15.4' - '15 : Xcode 16.2': - image: 'macOS-15' - xcode: '16.2' + xcode: '16.4' pool: vmImage: $(image) variables: From 125dd161cff2690057201c96a6969bdd82a8bb30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 15 Jun 2025 18:53:20 +0200 Subject: [PATCH 08/80] Support latest Swift versions --- Source/SwiftLintCore/Models/SwiftVersion.swift | 4 ++++ Tests/FrameworkTests/SwiftVersionTests.swift | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/SwiftLintCore/Models/SwiftVersion.swift b/Source/SwiftLintCore/Models/SwiftVersion.swift index 0a50d65c73..5082c9d337 100644 --- a/Source/SwiftLintCore/Models/SwiftVersion.swift +++ b/Source/SwiftLintCore/Models/SwiftVersion.swift @@ -69,6 +69,10 @@ public extension SwiftVersion { static let six = SwiftVersion(rawValue: "6.0.0") /// Swift 6.1 static let sixDotOne = SwiftVersion(rawValue: "6.1.0") + /// Swift 6.1.1 + static let sixDotOneDotOne = SwiftVersion(rawValue: "6.1.1") + /// Swift 6.1.2 + static let sixDotOneDotTwo = SwiftVersion(rawValue: "6.1.2") /// The current detected Swift compiler version, based on the currently accessible SourceKit version. /// diff --git a/Tests/FrameworkTests/SwiftVersionTests.swift b/Tests/FrameworkTests/SwiftVersionTests.swift index 17ab506cb5..e832e73777 100644 --- a/Tests/FrameworkTests/SwiftVersionTests.swift +++ b/Tests/FrameworkTests/SwiftVersionTests.swift @@ -3,7 +3,11 @@ import XCTest final class SwiftVersionTests: SwiftLintTestCase { func testDetectSwiftVersion() { -#if compiler(>=6.1.0) +#if compiler(>=6.1.2) + let version = "6.1.2" +#elseif compiler(>=6.1.1) + let version = "6.1.1" +#elseif compiler(>=6.1.0) let version = "6.1.0" #elseif compiler(>=6.0.3) let version = "6.0.3" From 348800b00776cd1cc03e99f05af02d2e530d635c Mon Sep 17 00:00:00 2001 From: Scott Hoyt Date: Sun, 15 Jun 2025 10:39:39 -0700 Subject: [PATCH 09/80] Update SwiftSyntax to 601.0.1 (#6093) --- MODULE.bazel | 2 +- Package.resolved | 4 ++-- Package.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index b18d64a226..579ebfabc6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -14,7 +14,7 @@ bazel_dep(name = "rules_shell", version = "0.4.0", repo_name = "build_bazel_rule bazel_dep(name = "rules_swift", version = "2.8.1", repo_name = "build_bazel_rules_swift") bazel_dep(name = "sourcekitten", version = "0.37.0", repo_name = "com_github_jpsim_sourcekitten") bazel_dep(name = "swift_argument_parser", version = "1.3.1.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") -bazel_dep(name = "swift-syntax", version = "601.0.0", repo_name = "SwiftSyntax") +bazel_dep(name = "swift-syntax", version = "601.0.1", repo_name = "SwiftSyntax") bazel_dep(name = "yams", version = "5.3.0", repo_name = "sourcekitten_com_github_jpsim_yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") diff --git a/Package.resolved b/Package.resolved index 82bf79fda6..dc9f55cbd6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "1103c45ece4f7fe160b8f75b4ea1ee2e5fac1841", - "version" : "601.0.0" + "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", + "version" : "601.0.1" } }, { diff --git a/Package.swift b/Package.swift index 81d6bc12f7..db70ee7211 100644 --- a/Package.swift +++ b/Package.swift @@ -33,7 +33,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.2.1")), - .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"), .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMajor(from: "0.37.0")), .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "5.3.0")), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", .upToNextMajor(from: "0.9.0")), From e2ef7ed4708fbf6328990473402b1e42f72f29b2 Mon Sep 17 00:00:00 2001 From: Sonal <87355487+imsonalbajaj@users.noreply.github.com> Date: Sun, 15 Jun 2025 23:26:09 +0530 Subject: [PATCH 10/80] Treat actors as classes in `class_delegate_protocol` rule (#6067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Mösch --- CHANGELOG.md | 4 +++- .../Rules/Lint/ClassDelegateProtocolRule.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8758375069..62c20c8bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ ### Enhancements -* None. +* Fix false positives for `Actor`-conforming delegate protocols in the `class_delegate_protocol` rule. + [imsonalbajaj](https://github.com/imsonalbajaj) + [#6054](https://github.com/realm/SwiftLint/issues/6054) ### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift index 32cb208480..e2dc375a36 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ClassDelegateProtocolRule.swift @@ -45,6 +45,7 @@ struct ClassDelegateProtocolRule: Rule { Example("protocol FooDelegate where Self: Foo & BarDelegate & Bar {}"), Example("protocol FooDelegate where Self: AnyObject {}"), Example("protocol FooDelegate where Self: NSObjectProtocol {}"), + Example("protocol FooDelegate: Actor {}"), ], triggeringExamples: [ Example("↓protocol FooDelegate {}"), @@ -107,7 +108,8 @@ private extension ProtocolDeclSyntax { private extension TypeSyntax { func isObjectOrDelegate() -> Bool { if let typeName = `as`(IdentifierTypeSyntax.self)?.typeName { - return typeName == "AnyObject" || typeName == "NSObjectProtocol" || typeName.hasSuffix("Delegate") + let objectTypes = ["AnyObject", "NSObjectProtocol", "Actor"] + return objectTypes.contains(typeName) || typeName.hasSuffix("Delegate") } if let combined = `as`(CompositionTypeSyntax.self) { return combined.elements.contains { $0.type.isObjectOrDelegate() } From f961ad172887838c93751f1bdc00024fc6374ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 15 Jun 2025 22:00:39 +0200 Subject: [PATCH 11/80] Prefer string blocks (#6090) --- .../ClosureEndIndentationRuleExamples.swift | 388 ++++++++++-------- 1 file changed, 227 insertions(+), 161 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift index 45c83d6d34..e49a4a7aca 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift @@ -1,173 +1,239 @@ internal struct ClosureEndIndentationRuleExamples { static let nonTriggeringExamples = [ - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - " }\n"), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + } + """), Example("[1, 2].map { $0 + 1 }\n"), - Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - "}.flatMap { command in\n" + - " return command.expand()\n" + - "}\n"), - Example("foo(foo: bar,\n" + - " options: baz) { _ in }\n"), - Example("someReallyLongProperty.chainingWithAnotherProperty\n" + - " .foo { _ in }"), - Example("foo(abc, 123)\n" + - "{ _ in }\n"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(parameter: param,\n" + - " closure: { x in\n" + - " print(x)\n" + - "})"), - Example("function(parameter: param, closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), + Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + }.flatMap { command in + return command.expand() + } + """), + Example(""" + foo(foo: bar, + options: baz) { _ in } + """), + Example(""" + someReallyLongProperty.chainingWithAnotherProperty + .foo { _ in } + """), + Example(""" + foo(abc, 123) + { _ in } + """), + Example(""" + function( + closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function(parameter: param, + closure: { x in + print(x) + }) + """), + Example(""" + function(parameter: param, closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), Example("(-variable).foo()"), ] static let triggeringExamples = [ - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - "↓}\n"), - Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - " ↓}.flatMap { command in\n" + - " return command.expand()\n" + - "↓}\n"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓},\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - "↓})"), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + ↓} + """), + Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + ↓}.flatMap { command in + return command.expand() + ↓} + """), + Example(""" + function( + closure: { x in + print(x) + ↓}, + anotherClosure: { y in + print(y) + ↓}) + """), ] static let corrections = [ - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - "↓}\n"): Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - " }\n"), - Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - "↓}.another { x in\n" + - " print(x)\n" + - "↓}.yetAnother { y in\n" + - " print(y)\n" + - "↓})"): Example("SignalProducer(values: [1, 2, 3])\n" + - " .startWithNext { number in\n" + - " print(number)\n" + - " }.another { x in\n" + - " print(x)\n" + - " }.yetAnother { y in\n" + - " print(y)\n" + - " })"), - Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - "↓ }.flatMap { command in\n" + - " return command.expand()\n" + - "↓}\n"): Example("return match(pattern: pattern, with: [.comment]).flatMap { range in\n" + - " return Command(string: contents, range: range)\n" + - "}.flatMap { command in\n" + - " return command.expand()\n" + - "}\n"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓})"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - "↓ print(x) })"): Example("function(\n" + - " closure: { x in\n" + - " print(x) \n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - "↓ab})"): Example("function(\n" + - " closure: { x in\n" + - "ab\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓},\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓ },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓ab},\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "ab\n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - "↓ print(x) },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"): Example("function(\n" + - " closure: { x in\n" + - " print(x) \n" + - " },\n" + - " anotherClosure: { y in\n" + - " print(y)\n" + - " })"), - Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - "↓}, anotherClosure: { y in\n" + - " print(y)\n" + - "↓})"): Example("function(\n" + - " closure: { x in\n" + - " print(x)\n" + - " }, anotherClosure: { y in\n" + - " print(y)\n" + - " })"), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + ↓} + """): Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + } + """), + Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + ↓}.another { x in + print(x) + ↓}.yetAnother { y in + print(y) + ↓}) + """): Example(""" + SignalProducer(values: [1, 2, 3]) + .startWithNext { number in + print(number) + }.another { x in + print(x) + }.yetAnother { y in + print(y) + }) + """), + Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + ↓ }.flatMap { command in + return command.expand() + ↓} + """): Example(""" + return match(pattern: pattern, with: [.comment]).flatMap { range in + return Command(string: contents, range: range) + }.flatMap { command in + return command.expand() + } + """), + Example(""" + function( + closure: { x in + print(x) + ↓}) + """): Example(""" + function( + closure: { x in + print(x) + }) + """), + Example(""" + function( + closure: { x in + ↓ print(x) }) + """): Example(""" + function( + closure: { x in + print(x) \("") + }) + """), + Example(""" + function( + closure: { x in + ↓ab}) + """): Example(""" + function( + closure: { x in + ab + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓}, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓ }, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓ab}, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) + ab + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + ↓ print(x) }, + anotherClosure: { y in + print(y) + }) + """): Example(""" + function( + closure: { x in + print(x) \("") + }, + anotherClosure: { y in + print(y) + }) + """), + Example(""" + function( + closure: { x in + print(x) + ↓}, anotherClosure: { y in + print(y) + ↓}) + """): Example(""" + function( + closure: { x in + print(x) + }, anotherClosure: { y in + print(y) + }) + """), ] } From 44f0cbbcc463d35ff899824ac23089c5eb637e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 15 Jun 2025 22:01:03 +0200 Subject: [PATCH 12/80] Fix markdownlint violations --- CHANGELOG.md | 7 +++---- README.md | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c20c8bdb..93b6f87606 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3852,10 +3852,9 @@ This is the last release to support building with Swift 5.0.x. * API docs for SwiftLintFramework are now available at [realm.github.io/SwiftLint](https://realm.github.io/SwiftLint). `Rules.md` - now redirects to the rules directory in the API docs - [here](https://realm.github.io/SwiftLint/rule-directory.html). Contributors no - longer need to update rule documentation in PRs as this is now done - automatically. The rule documentation now includes the default configuration. + now redirects to the [Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html) + in the API docs. Contributors no longer need to update rule documentation in PRs as this is + now done automatically. The rule documentation now includes the default configuration. [JP Simard](https://github.com/jpsim) [#1653](https://github.com/realm/SwiftLint/issues/1653) [#1704](https://github.com/realm/SwiftLint/issues/1704) diff --git a/README.md b/README.md index 73ec013550..7ea22543f5 100644 --- a/README.md +++ b/README.md @@ -596,8 +596,8 @@ continues to contribute more over time. [Pull requests](https://github.com/realm/SwiftLint/blob/main/CONTRIBUTING.md) are encouraged. -You can find an updated list of rules and more information about them -[here](https://realm.github.io/SwiftLint/rule-directory.html). +You can find an updated list of rules and more information about them in the +[Rule Directory](https://realm.github.io/SwiftLint/rule-directory.html). You can also check the [Source/SwiftLintBuiltInRules/Rules](https://github.com/realm/SwiftLint/tree/main/Source/SwiftLintBuiltInRules/Rules) From d3768ef9e94e71211b79eb84cfc00ae1909ca762 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Jun 2025 20:21:54 +0000 Subject: [PATCH 13/80] Bump DavidAnson/markdownlint-cli2-action (#6083) Bumps the github-actions group with 1 update: [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action). Updates `DavidAnson/markdownlint-cli2-action` from 19 to 20 - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v19...v20) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-version: '20' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8b2c429a6f..3730369b46 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Lint - uses: DavidAnson/markdownlint-cli2-action@v19 + uses: DavidAnson/markdownlint-cli2-action@v20 with: globs: | CHANGELOG.md From bf935b022616dcccc1158330a27fabd4502efa91 Mon Sep 17 00:00:00 2001 From: Luis Padron Date: Mon, 16 Jun 2025 13:48:25 -0400 Subject: [PATCH 14/80] Support rules_swift 3.0 (#6096) --- .bazelversion | 2 +- BUILD | 2 +- MODULE.bazel | 14 +++++++------- bazel/repos.bzl | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.bazelversion b/.bazelversion index 66ce77b7ea..e8be68404b 100644 --- a/.bazelversion +++ b/.bazelversion @@ -1 +1 @@ -7.0.0 +7.6.1 diff --git a/BUILD b/BUILD index e862723d18..23d9b29112 100644 --- a/BUILD +++ b/BUILD @@ -7,7 +7,7 @@ load( "swift_library", "universal_swift_compiler_plugin", ) -load("@build_bazel_rules_cc//cc:cc_library.bzl", "cc_library") +load("@rules_cc//cc:cc_library.bzl", "cc_library") load("@build_bazel_rules_shell//shell:sh_test.bzl", "sh_test") bool_flag( diff --git a/MODULE.bazel b/MODULE.bazel index 579ebfabc6..6f91a72ba3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -8,14 +8,14 @@ module( bazel_dep(name = "apple_support", version = "1.16.0", repo_name = "build_bazel_apple_support") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "platforms", version = "0.0.10") -bazel_dep(name = "rules_apple", version = "3.20.1", repo_name = "build_bazel_rules_apple") -bazel_dep(name = "rules_cc", version = "0.1.1", repo_name = "build_bazel_rules_cc") +bazel_dep(name = "rules_apple", version = "4.0.1", repo_name = "build_bazel_rules_apple") +bazel_dep(name = "rules_cc", version = "0.1.1") bazel_dep(name = "rules_shell", version = "0.4.0", repo_name = "build_bazel_rules_shell") -bazel_dep(name = "rules_swift", version = "2.8.1", repo_name = "build_bazel_rules_swift") -bazel_dep(name = "sourcekitten", version = "0.37.0", repo_name = "com_github_jpsim_sourcekitten") -bazel_dep(name = "swift_argument_parser", version = "1.3.1.1", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") -bazel_dep(name = "swift-syntax", version = "601.0.1", repo_name = "SwiftSyntax") -bazel_dep(name = "yams", version = "5.3.0", repo_name = "sourcekitten_com_github_jpsim_yams") +bazel_dep(name = "rules_swift", version = "2.8.1", max_compatibility_level = 3, repo_name = "build_bazel_rules_swift") +bazel_dep(name = "sourcekitten", version = "0.37.2", repo_name = "com_github_jpsim_sourcekitten") +bazel_dep(name = "swift_argument_parser", version = "1.3.1.2", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") +bazel_dep(name = "swift-syntax", version = "601.0.1.1", repo_name = "SwiftSyntax") +bazel_dep(name = "yams", version = "6.0.1", repo_name = "sourcekitten_com_github_jpsim_yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") use_repo( diff --git a/bazel/repos.bzl b/bazel/repos.bzl index 14a928c8fb..9101418a2f 100644 --- a/bazel/repos.bzl +++ b/bazel/repos.bzl @@ -5,16 +5,16 @@ def swiftlint_repos(bzlmod = False): if not bzlmod: http_archive( name = "com_github_jpsim_sourcekitten", - sha256 = "d9c559166f01627826505b0e655b56a59f86938389e1739259e6ce49c9fd95f0", - strip_prefix = "SourceKitten-0.37.0", - url = "https://github.com/jpsim/SourceKitten/releases/download/0.37.0/SourceKitten-0.37.0.tar.gz", + sha256 = "38d62bf1114c878a017f1c685ff7e98413390591c93f354a16ed751a8b0bf87f", + strip_prefix = "SourceKitten-0.37.1", + url = "https://github.com/jpsim/SourceKitten/releases/download/0.37.1/SourceKitten-0.37.1.tar.gz", ) http_archive( name = "SwiftSyntax", - sha256 = "6572f60ca3c75c2a40f8ccec98c5cd0d3994599a39402d69b433381aaf2c1712", - strip_prefix = "swift-syntax-600.0.0", - url = "https://github.com/swiftlang/swift-syntax/archive/refs/tags/600.0.0.tar.gz", + sha256 = "02450ab3fd1d676fffd3719f3263293c51d567cae741fc340c68930388781286", + strip_prefix = "swift-syntax-601.0.1", + url = "https://github.com/swiftlang/swift-syntax/archive/refs/tags/601.0.1.tar.gz", ) http_archive( @@ -27,16 +27,16 @@ def swiftlint_repos(bzlmod = False): http_archive( name = "sourcekitten_com_github_jpsim_yams", - url = "https://github.com/jpsim/Yams/releases/download/5.3.0/Yams-5.3.0.tar.gz", - sha256 = "a81c6b93f5d26bae1b619b7f8babbfe7c8abacf95b85916961d488888df886fb", - strip_prefix = "Yams-5.3.0", + url = "https://github.com/jpsim/Yams/releases/download/6.0.1/Yams-6.0.1.tar.gz", + sha256 = "76afe79db05acb0eda4910e0b9da6a8562ad6139ac317daa747cd829beb93b9e", + strip_prefix = "Yams-6.0.1", ) http_archive( name = "sourcekitten_com_github_drmohundro_SWXMLHash", url = "https://github.com/drmohundro/SWXMLHash/archive/refs/tags/7.0.2.tar.gz", build_file = "@com_github_jpsim_sourcekitten//bazel:SWXMLHash.BUILD", - sha256 = "bafa037a09aa296f180e5613206748db5053b79aa09258c78d093ae9f8102a18", + sha256 = "d7d600f062d6840b037fc1fb2ac3afce7a1c43ae430d78e22d7bd6f8e02cfc9d", strip_prefix = "SWXMLHash-7.0.2", ) From 66930722ada01f630d4971be9260794bd6ad6ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Tue, 17 Jun 2025 10:22:49 +0200 Subject: [PATCH 15/80] Update dependencies and harmonize Bazel build modes (#6097) --- BUILD | 10 +++++----- MODULE.bazel | 12 ++++++------ Package.resolved | 12 ++++++------ Package.swift | 6 +++--- bazel/SWXMLHash.BUILD | 11 +++++++++++ bazel/SwiftArgumentParser.BUILD | 17 +++++++++++++++++ bazel/repos.bzl | 26 +++++++++++++------------- 7 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 bazel/SWXMLHash.BUILD create mode 100644 bazel/SwiftArgumentParser.BUILD diff --git a/BUILD b/BUILD index 23d9b29112..6176354af0 100644 --- a/BUILD +++ b/BUILD @@ -99,7 +99,7 @@ swift_library( ":Yams.wrapper", "@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable", ] + select({ - "@platforms//os:linux": ["@com_github_krzyzanowskim_cryptoswift//:CryptoSwift"], + "@platforms//os:linux": ["@swiftlint_com_github_krzyzanowskim_cryptoswift//:CryptoSwift"], "//conditions:default": [":DyldWarningWorkaround"], }), ) @@ -110,7 +110,7 @@ swift_library( module_name = "YamsWrapper", visibility = ["//visibility:private"], deps = [ - "@sourcekitten_com_github_jpsim_yams//:Yams", + "@com_github_jpsim_yams//:Yams", ], ) @@ -154,7 +154,7 @@ swift_library( ":SwiftLintBuiltInRules", ":SwiftLintCore", ":SwiftLintExtraRules", - "@com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit", + "@swiftlint_com_github_johnsundell_collectionconcurrencykit//:CollectionConcurrencyKit", ], ) @@ -166,7 +166,7 @@ swift_binary( visibility = ["//visibility:public"], deps = [ ":SwiftLintFramework", - "@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser", + "@com_github_apple_swift_argument_parser//:ArgumentParser", "@swiftlint_com_github_scottrhoyt_swifty_text_table//:SwiftyTextTable", ], ) @@ -179,7 +179,7 @@ swift_binary( visibility = ["//visibility:public"], deps = [ ":SwiftLintFramework", - "@sourcekitten_com_github_apple_swift_argument_parser//:ArgumentParser", + "@com_github_apple_swift_argument_parser//:ArgumentParser", ], ) diff --git a/MODULE.bazel b/MODULE.bazel index 6f91a72ba3..31c22c5ef1 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -5,23 +5,23 @@ module( repo_name = "SwiftLint", ) -bazel_dep(name = "apple_support", version = "1.16.0", repo_name = "build_bazel_apple_support") +bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support") bazel_dep(name = "bazel_skylib", version = "1.7.1") -bazel_dep(name = "platforms", version = "0.0.10") +bazel_dep(name = "platforms", version = "0.0.11") bazel_dep(name = "rules_apple", version = "4.0.1", repo_name = "build_bazel_rules_apple") bazel_dep(name = "rules_cc", version = "0.1.1") bazel_dep(name = "rules_shell", version = "0.4.0", repo_name = "build_bazel_rules_shell") bazel_dep(name = "rules_swift", version = "2.8.1", max_compatibility_level = 3, repo_name = "build_bazel_rules_swift") bazel_dep(name = "sourcekitten", version = "0.37.2", repo_name = "com_github_jpsim_sourcekitten") -bazel_dep(name = "swift_argument_parser", version = "1.3.1.2", repo_name = "sourcekitten_com_github_apple_swift_argument_parser") +bazel_dep(name = "swift_argument_parser", version = "1.5.0", repo_name = "com_github_apple_swift_argument_parser") bazel_dep(name = "swift-syntax", version = "601.0.1.1", repo_name = "SwiftSyntax") -bazel_dep(name = "yams", version = "6.0.1", repo_name = "sourcekitten_com_github_jpsim_yams") +bazel_dep(name = "yams", version = "6.0.1", repo_name = "com_github_jpsim_yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") use_repo( swiftlint_repos, - "com_github_johnsundell_collectionconcurrencykit", - "com_github_krzyzanowskim_cryptoswift", + "swiftlint_com_github_johnsundell_collectionconcurrencykit", + "swiftlint_com_github_krzyzanowskim_cryptoswift", "swiftlint_com_github_scottrhoyt_swifty_text_table", ) diff --git a/Package.resolved b/Package.resolved index dc9f55cbd6..f191f6b728 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { - "revision" : "eb6656ed26bdef967ad8d07c27e2eab34dc582f2", - "version" : "0.37.0" + "revision" : "731ffe6a35344a19bab00cdca1c952d5b4fee4d8", + "version" : "0.37.2" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "41982a3656a71c768319979febd796c6fd111d5c", - "version" : "1.5.0" + "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", + "version" : "1.5.1" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { - "revision" : "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", - "version" : "5.3.1" + "revision" : "7568d1c6c63a094405afb32264c57dc4e1435835", + "version" : "6.0.1" } } ], diff --git a/Package.swift b/Package.swift index db70ee7211..fabb7d323a 100644 --- a/Package.swift +++ b/Package.swift @@ -32,10 +32,10 @@ let package = Package( .plugin(name: "SwiftLintCommandPlugin", targets: ["SwiftLintCommandPlugin"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.2.1")), + .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.5.1")), .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"), - .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMajor(from: "0.37.0")), - .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "5.3.0")), + .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMajor(from: "0.37.2")), + .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "6.0.1")), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", .upToNextMajor(from: "0.9.0")), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", .upToNextMajor(from: "0.2.0")), .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.4")), diff --git a/bazel/SWXMLHash.BUILD b/bazel/SWXMLHash.BUILD new file mode 100644 index 0000000000..42d6233a58 --- /dev/null +++ b/bazel/SWXMLHash.BUILD @@ -0,0 +1,11 @@ +load( + "@build_bazel_rules_swift//swift:swift.bzl", + "swift_library", +) + +swift_library( + name = "SWXMLHash", + srcs = glob(["Source/**/*.swift"]), + module_name = "SWXMLHash", + visibility = ["//visibility:public"], +) diff --git a/bazel/SwiftArgumentParser.BUILD b/bazel/SwiftArgumentParser.BUILD new file mode 100644 index 0000000000..7db3aee8db --- /dev/null +++ b/bazel/SwiftArgumentParser.BUILD @@ -0,0 +1,17 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ArgumentParserToolInfo", + srcs = glob(["Sources/ArgumentParserToolInfo/**/*.swift"]), + module_name = "ArgumentParserToolInfo", +) + +swift_library( + name = "ArgumentParser", + srcs = glob(["Sources/ArgumentParser/**/*.swift"]), + module_name = "ArgumentParser", + visibility = ["//visibility:public"], + deps = [ + ":ArgumentParserToolInfo", + ], +) diff --git a/bazel/repos.bzl b/bazel/repos.bzl index 9101418a2f..760d8aded9 100644 --- a/bazel/repos.bzl +++ b/bazel/repos.bzl @@ -5,9 +5,9 @@ def swiftlint_repos(bzlmod = False): if not bzlmod: http_archive( name = "com_github_jpsim_sourcekitten", - sha256 = "38d62bf1114c878a017f1c685ff7e98413390591c93f354a16ed751a8b0bf87f", - strip_prefix = "SourceKitten-0.37.1", - url = "https://github.com/jpsim/SourceKitten/releases/download/0.37.1/SourceKitten-0.37.1.tar.gz", + sha256 = "604d2e5e547ef4280c959760cba0c9bd9be759c9555796cf7a73d9e1c9bcfc90", + strip_prefix = "SourceKitten-0.37.2", + url = "https://github.com/jpsim/SourceKitten/releases/download/0.37.2/SourceKitten-0.37.2.tar.gz", ) http_archive( @@ -18,24 +18,24 @@ def swiftlint_repos(bzlmod = False): ) http_archive( - name = "sourcekitten_com_github_apple_swift_argument_parser", - url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.3.1.tar.gz", - sha256 = "4d964f874b251abc280ee28f0f187de3c13a6122a9561524f66a10768ca2d837", - build_file = "@com_github_jpsim_sourcekitten//bazel:SwiftArgumentParser.BUILD", - strip_prefix = "swift-argument-parser-1.3.1", + name = "com_github_apple_swift_argument_parser", + url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.5.0.tar.gz", + build_file = "@SwiftLint//bazel:SwiftArgumentParser.BUILD", + sha256 = "946a4cf7bdd2e4f0f8b82864c56332238ba3f0a929c6d1a15f55affdb10634e6", + strip_prefix = "swift-argument-parser-1.5.0", ) http_archive( - name = "sourcekitten_com_github_jpsim_yams", + name = "com_github_jpsim_yams", url = "https://github.com/jpsim/Yams/releases/download/6.0.1/Yams-6.0.1.tar.gz", sha256 = "76afe79db05acb0eda4910e0b9da6a8562ad6139ac317daa747cd829beb93b9e", strip_prefix = "Yams-6.0.1", ) http_archive( - name = "sourcekitten_com_github_drmohundro_SWXMLHash", + name = "com_github_drmohundro_SWXMLHash", url = "https://github.com/drmohundro/SWXMLHash/archive/refs/tags/7.0.2.tar.gz", - build_file = "@com_github_jpsim_sourcekitten//bazel:SWXMLHash.BUILD", + build_file = "@SwiftLint//bazel:SWXMLHash.BUILD", sha256 = "d7d600f062d6840b037fc1fb2ac3afce7a1c43ae430d78e22d7bd6f8e02cfc9d", strip_prefix = "SWXMLHash-7.0.2", ) @@ -49,7 +49,7 @@ def swiftlint_repos(bzlmod = False): ) http_archive( - name = "com_github_johnsundell_collectionconcurrencykit", + name = "swiftlint_com_github_johnsundell_collectionconcurrencykit", sha256 = "9083fe6f8b4f820bfb5ef5c555b31953116f158ec113e94c6406686e78da34aa", build_file = "@SwiftLint//bazel:CollectionConcurrencyKit.BUILD", strip_prefix = "CollectionConcurrencyKit-0.2.0", @@ -57,7 +57,7 @@ def swiftlint_repos(bzlmod = False): ) http_archive( - name = "com_github_krzyzanowskim_cryptoswift", + name = "swiftlint_com_github_krzyzanowskim_cryptoswift", sha256 = "69b23102ff453990d03aff4d3fabd172d0667b2b3ed95730021d60a0f8d50d14", build_file = "@SwiftLint//bazel:CryptoSwift.BUILD", strip_prefix = "CryptoSwift-1.8.4", From d1687859feb559efd44db89aeb691cb596be3c84 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Wed, 18 Jun 2025 18:17:47 -0400 Subject: [PATCH 16/80] Migrate FileLengthRule from SourceKit to SwiftSyntax (#6100) Convert FileLengthRule to use SwiftSyntax instead of SourceKit for improved performance and fewer false positives. The SwiftSyntax implementation: - Uses ViolationsSyntaxVisitor pattern with token traversal - Correctly handles multiline tokens by counting all spanned lines - Properly excludes comment-only and whitespace-only lines - Accurately attributes trivia to correct line positions - Extracts common line range logic to reduce code duplication --- CHANGELOG.md | 4 + .../Rules/Metrics/FileLengthRule.swift | 98 ++++++++++++++----- 2 files changed, 77 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b6f87606..f4db433e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ [imsonalbajaj](https://github.com/imsonalbajaj) [#6054](https://github.com/realm/SwiftLint/issues/6054) +* Migrate `file_length` rule from SourceKit to SwiftSyntax for improved performance + and fewer false positives. + [JP Simard](https://github.com/jpsim) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift index 6679fec0bf..54f9c889da 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FileLengthRule.swift @@ -1,5 +1,6 @@ -import SourceKittenFramework +import SwiftSyntax +@SwiftSyntaxRule struct FileLengthRule: Rule { var configuration = FileLengthConfiguration() @@ -17,38 +18,85 @@ struct FileLengthRule: Rule { Example(repeatElement("print(\"swiftlint\")\n\n", count: 201).joined()), ].skipWrappingInCommentTests() ) +} + +private extension FileLengthRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: SourceFileSyntax) { + let lineCount = configuration.ignoreCommentOnlyLines ? countNonCommentLines(in: node) : file.lines.count + + let severity: ViolationSeverity, upperBound: Int + if let error = configuration.severityConfiguration.error, lineCount > error { + severity = .error + upperBound = error + } else if lineCount > configuration.severityConfiguration.warning { + severity = .warning + upperBound = configuration.severityConfiguration.warning + } else { + return + } + + let reason = "File should contain \(upperBound) lines or less" + + (configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") + + ": currently contains \(lineCount)" + + // Position violation at the start of the last line to avoid boundary issues + let lastLine = file.lines.last + let lastLineStartOffset = lastLine?.byteRange.location ?? 0 + let violationPosition = AbsolutePosition(utf8Offset: lastLineStartOffset.value) - func validate(file: SwiftLintFile) -> [StyleViolation] { - func lineCountWithoutComments() -> Int { - let commentKinds = SyntaxKind.commentKinds - return file.syntaxKindsByLines.filter { kinds in - !Set(kinds).isSubset(of: commentKinds) - }.count + let violation = ReasonedRuleViolation( + position: violationPosition, + reason: reason, + severity: severity + ) + violations.append(violation) } - var lineCount = file.lines.count - let hasViolation = configuration.severityConfiguration.params.contains { - $0.value < lineCount + private func countNonCommentLines(in node: SourceFileSyntax) -> Int { + var linesWithActualContent = Set() + + for token in node.tokens(viewMode: .sourceAccurate) { + addTokenContentLines(token, to: &linesWithActualContent) + + // Process leading trivia + addTriviaLines(token.leadingTrivia, startingAt: token.position, to: &linesWithActualContent) + } + return linesWithActualContent.count } - if hasViolation && configuration.ignoreCommentOnlyLines { - lineCount = lineCountWithoutComments() + private func addTokenContentLines(_ token: TokenSyntax, to lines: inout Set) { + // Skip tokens whose text is empty or only whitespace + // (e.g., EOF token, or an unlikely malformed token). + guard !token.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return } + + let startLocation = locationConverter.location(for: token.positionAfterSkippingLeadingTrivia) + let endLocation = locationConverter.location(for: token.endPositionBeforeTrailingTrivia) + + addLinesInRange(from: startLocation.line, to: endLocation.line, to: &lines) } - for parameter in configuration.severityConfiguration.params where lineCount > parameter.value { - let reason = "File should contain \(configuration.severityConfiguration.warning) lines or less" + - (configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") + - ": currently contains \(lineCount)" - return [ - StyleViolation( - ruleDescription: Self.description, - severity: parameter.severity, - location: Location(file: file.path, line: file.lines.count), - reason: reason - ), - ] + private func addTriviaLines( + _ trivia: Trivia, + startingAt startPosition: AbsolutePosition, + to lines: inout Set + ) { + var currentPosition = startPosition + for piece in trivia { + if !piece.isComment && !piece.isWhitespace { + let startLocation = locationConverter.location(for: currentPosition) + let endLocation = locationConverter.location(for: currentPosition + piece.sourceLength) + addLinesInRange(from: startLocation.line, to: endLocation.line, to: &lines) + } + currentPosition += piece.sourceLength + } } - return [] + private func addLinesInRange(from startLine: Int, to endLine: Int, to lines: inout Set) { + guard startLine > 0 && startLine <= endLine else { return } + for line in startLine...endLine { + lines.insert(line) + } + } } } From 6d5af5f924c4a9998822c008ab75d94c1c314e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Thu, 19 Jun 2025 21:01:42 +0200 Subject: [PATCH 17/80] Enable `async_without_await` rule (#6104) --- .swiftlint.yml | 1 - .../SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift | 2 +- Source/SwiftLintFramework/Configuration+CommandLine.swift | 6 +++--- Source/swiftlint-dev/Reporters+Register.swift | 2 +- Source/swiftlint-dev/Rules+Register.swift | 2 +- Source/swiftlint-dev/Rules+Template.swift | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 063f69e000..dcb93c99d8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -16,7 +16,6 @@ opt_in_rules: - all disabled_rules: - anonymous_argument_in_multiline_closure - - async_without_await - conditional_returns_on_newline - contrasted_opening_brace - convenience_type diff --git a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift index 2005c4dfda..211e28a21a 100644 --- a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift +++ b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift @@ -6,7 +6,7 @@ struct SwiftLintBuildToolPlugin: BuildToolPlugin { func createBuildCommands( context: PluginContext, target: Target - ) async throws -> [Command] { + ) throws -> [Command] { try makeCommand(executable: context.tool(named: "swiftlint"), swiftFiles: (target as? SourceModuleTarget).flatMap(swiftFiles) ?? [], environment: environment(context: context, target: target), diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index d33c732a5f..8807996788 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -75,8 +75,8 @@ private func autoreleasepool(block: () -> T) -> T { block() } extension Configuration { func visitLintableFiles(with visitor: LintableFilesVisitor, storage: RuleStorage) async throws -> [SwiftLintFile] { - let files = try await Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") { - try await getFiles(with: visitor) + let files = try Signposts.record(name: "Configuration.VisitLintableFiles.GetFiles") { + try getFiles(with: visitor) } let groupedFiles = try Signposts.record(name: "Configuration.VisitLintableFiles.GroupFiles") { try groupFiles(files, visitor: visitor) @@ -236,7 +236,7 @@ extension Configuration { linters.asyncMap(visit) } - fileprivate func getFiles(with visitor: LintableFilesVisitor) async throws -> [SwiftLintFile] { + fileprivate func getFiles(with visitor: LintableFilesVisitor) throws -> [SwiftLintFile] { let options = visitor.options if options.useSTDIN { let stdinData = FileHandle.standardInput.readDataToEndOfFile() diff --git a/Source/swiftlint-dev/Reporters+Register.swift b/Source/swiftlint-dev/Reporters+Register.swift index 118de060ff..ed4fe57cdb 100644 --- a/Source/swiftlint-dev/Reporters+Register.swift +++ b/Source/swiftlint-dev/Reporters+Register.swift @@ -18,7 +18,7 @@ extension SwiftLintDev.Reporters { .appendingPathComponent("Reporters", isDirectory: true) } - func run() async throws { + func run() throws { guard FileManager.default.fileExists(atPath: reportersDirectory.path) else { throw ValidationError("Command must be run from the root of the SwiftLint repository.") } diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index a8a87a1c06..2507e27b2e 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -26,7 +26,7 @@ extension SwiftLintDev.Rules { .appendingPathComponent("GeneratedTests", isDirectory: true) } - func run() async throws { + func run() throws { try runFor(newRule: nil) } diff --git a/Source/swiftlint-dev/Rules+Template.swift b/Source/swiftlint-dev/Rules+Template.swift index 05fd8db57f..229534ee47 100644 --- a/Source/swiftlint-dev/Rules+Template.swift +++ b/Source/swiftlint-dev/Rules+Template.swift @@ -42,7 +42,7 @@ extension SwiftLintDev.Rules { @Flag(name: .long, help: "Skip registration.") var skipRegistration = false - func run() async throws { + func run() throws { let rootDirectory = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) let ruleDirectory = rootDirectory .appendingPathComponent("Source", isDirectory: true) From 1e25cf6be68dd2e62df974f5642cd37cb74eadce Mon Sep 17 00:00:00 2001 From: Matt Pennig Date: Thu, 19 Jun 2025 16:31:51 -0500 Subject: [PATCH 18/80] Migrate VerticalWhitespaceRule from SourceKit to SwiftSyntax (#6103) * Migrate VerticalWhitespaceRule from SourceKit to SwiftSyntax * Adds tests for new horizontal whitespace behavior --- CHANGELOG.md | 3 + .../VerticalWhitespaceConfiguration.swift | 9 + .../Rules/Style/VerticalWhitespaceRule.swift | 207 +++++++++--------- .../VerticalWhitespaceRuleTests.swift | 6 +- 4 files changed, 121 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4db433e87..4794154854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ and fewer false positives. [JP Simard](https://github.com/jpsim) +* Migrate `vertical_whitespace` rule from SourceKit to SwiftSyntax for improved performance. + [Matt Pennig](https://github.com/pennig) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift index 2bc989d73f..676a524510 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/VerticalWhitespaceConfiguration.swift @@ -4,8 +4,17 @@ import SwiftLintCore struct VerticalWhitespaceConfiguration: SeverityBasedRuleConfiguration { typealias Parent = VerticalWhitespaceRule + static let defaultDescriptionReason = "Limit vertical whitespace to a single empty line" + @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) @ConfigurationElement(key: "max_empty_lines") private(set) var maxEmptyLines = 1 + + var configuredDescriptionReason: String { + guard maxEmptyLines == 1 else { + return "Limit vertical whitespace to maximum \(maxEmptyLines) empty lines" + } + return Self.defaultDescriptionReason + } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 2ff363264a..4e1c827e04 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -1,146 +1,147 @@ import Foundation -import SourceKittenFramework +import SwiftSyntax -private let defaultDescriptionReason = "Limit vertical whitespace to a single empty line" - -struct VerticalWhitespaceRule: CorrectableRule { +@SwiftSyntaxRule(explicitRewriter: true, correctable: true) +struct VerticalWhitespaceRule: Rule { var configuration = VerticalWhitespaceConfiguration() static let description = RuleDescription( identifier: "vertical_whitespace", name: "Vertical Whitespace", - description: defaultDescriptionReason + ".", + description: VerticalWhitespaceConfiguration.defaultDescriptionReason, kind: .style, nonTriggeringExamples: [ Example("let abc = 0\n"), Example("let abc = 0\n\n"), Example("/* bcs \n\n\n\n*/"), Example("// bca \n\n"), + Example("class CCCC {\n \n}"), ], triggeringExamples: [ Example("let aaaa = 0\n\n\n"), Example("struct AAAA {}\n\n\n\n"), Example("class BBBB {}\n\n\n"), + Example("class CCCC {\n \n \n}"), ], corrections: [ Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\nclass AAA {}\n"), Example("let c = 0\n\n\nlet num = 1\n"): Example("let c = 0\n\nlet num = 1\n"), Example("// bca \n\n\n"): Example("// bca \n\n"), + Example("class CCCC {\n \n \n \n}"): Example("class CCCC {\n \n}"), ] // End of line autocorrections are handled by Trailing Newline Rule. ) +} - private var configuredDescriptionReason: String { - guard configuration.maxEmptyLines == 1 else { - return "Limit vertical whitespace to maximum \(configuration.maxEmptyLines) empty lines" - } - return defaultDescriptionReason - } - - func validate(file: SwiftLintFile) -> [StyleViolation] { - let linesSections = violatingLineSections(in: file) - guard linesSections.isNotEmpty else { - return [] - } - - return linesSections.map { eachLastLine, eachSectionCount in - StyleViolation( - ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: Location(file: file.path, line: eachLastLine.index), - reason: configuredDescriptionReason + "; currently \(eachSectionCount + 1)" - ) - } - } - - private typealias LineSection = (lastLine: Line, linesToRemove: Int) +private extension VerticalWhitespaceRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind { + // The strategy here is to keep track of the position of the _first_ violating newline + // in each consecutive run, and report the violation when the run _ends_. - private func violatingLineSections(in file: SwiftLintFile) -> [LineSection] { - let nonSpaceRegex = regex("\\S", options: []) - let filteredLines = file.lines.filter { - nonSpaceRegex.firstMatch(in: file.contents, options: [], range: $0.range) == nil - } - - guard filteredLines.isNotEmpty else { - return [] - } + if token.leadingTrivia.isEmpty { + return .visitChildren + } - let blankLinesSections = extractSections(from: filteredLines) + var consecutiveNewlines = 0 + var currentPosition = token.position + var violationPosition: AbsolutePosition? + + func process(_ count: Int, _ offset: Int) { + for _ in 0.. configuration.maxEmptyLines && violationPosition == nil { + violationPosition = currentPosition + } + consecutiveNewlines += 1 + currentPosition = currentPosition.advanced(by: offset) + } + } - // filtering out violations in comments and strings - let stringAndComments = SyntaxKind.commentAndStringKinds - let syntaxMap = file.syntaxMap - let result = blankLinesSections.compactMap { eachSection -> (lastLine: Line, linesToRemove: Int)? in - guard let lastLine = eachSection.last else { - return nil + for piece in token.leadingTrivia { + switch piece { + case .newlines(let count), .carriageReturns(let count), .formfeeds(let count), .verticalTabs(let count): + process(count, 1) + case .carriageReturnLineFeeds(let count): + process(count, 2) // CRLF is 2 bytes + case .spaces, .tabs: + currentPosition += piece.sourceLength + default: + if let violationPosition { + report(violationPosition, consecutiveNewlines) + } + violationPosition = nil + consecutiveNewlines = 0 + currentPosition += piece.sourceLength + } } - let kindInSection = syntaxMap.kinds(inByteRange: lastLine.byteRange) - if stringAndComments.isDisjoint(with: kindInSection) { - return (lastLine, eachSection.count) + if let violationPosition { + report(violationPosition, consecutiveNewlines) } - return nil + return .visitChildren } - return result.filter { $0.linesToRemove >= configuration.maxEmptyLines } - } - - private func extractSections(from lines: [Line]) -> [[Line]] { - var blankLinesSections = [[Line]]() - var lineSection = [Line]() - - var previousIndex = 0 - for (index, line) in lines.enumerated() { - let previousLine: Line = lines[previousIndex] - if previousLine.index + 1 == line.index { - lineSection.append(line) - } else if lineSection.isNotEmpty { - blankLinesSections.append(lineSection) - lineSection.removeAll() - } - previousIndex = index - } - if lineSection.isNotEmpty { - blankLinesSections.append(lineSection) + private func report(_ position: AbsolutePosition, _ newlines: Int) { + violations.append(ReasonedRuleViolation( + position: position, + reason: configuration.configuredDescriptionReason + "; currently \(newlines - 1)" + )) } - - return blankLinesSections } - func correct(file: SwiftLintFile) -> Int { - let linesSections = violatingLineSections(in: file) - if linesSections.isEmpty { - return 0 - } - - var indexOfLinesToDelete = [Int]() - - for section in linesSections { - let linesToRemove = section.linesToRemove - configuration.maxEmptyLines + 1 - let start = section.lastLine.index - linesToRemove - indexOfLinesToDelete.append(contentsOf: start.. { + override func visit(_ token: TokenSyntax) -> TokenSyntax { + var result = [TriviaPiece]() + var pendingWhitespace = [TriviaPiece]() + var consecutiveNewlines = 0 + + func process(_ count: Int, _ create: (Int) -> TriviaPiece) { + let linesToPreserve = min(count, max(0, configuration.maxEmptyLines + 1 - consecutiveNewlines)) + consecutiveNewlines += count + + if count > linesToPreserve { + self.numberOfCorrections += count - linesToPreserve + } + + if linesToPreserve > 0 { + // We can still add this piece, even if we adjusted its count lower. + // Pull in any pending whitespace along with it. + result.append(contentsOf: pendingWhitespace) + result.append(create(linesToPreserve)) + pendingWhitespace.removeAll() + } else { + // We're now in violation. Dump pending whitespace so it's excluded from the result. + pendingWhitespace.removeAll() + } + } - var correctedLines = [String]() - var numberOfCorrections = 0 - for currentLine in file.lines { - // Doesn't correct lines where rule is disabled - if file.ruleEnabled(violatingRanges: [currentLine.range], for: self).isEmpty { - correctedLines.append(currentLine.content) - continue + for piece in token.leadingTrivia { + switch piece { + case .newlines(let count): + process(count, TriviaPiece.newlines) + case .carriageReturns(let count): + process(count, TriviaPiece.carriageReturns) + case .carriageReturnLineFeeds(let count): + process(count, TriviaPiece.carriageReturnLineFeeds) + case .formfeeds(let count): + process(count, TriviaPiece.formfeeds) + case .verticalTabs(let count): + process(count, TriviaPiece.verticalTabs) + case .spaces, .tabs: + pendingWhitespace.append(piece) + default: + // Reset and pull in pending whitespace + consecutiveNewlines = 0 + result.append(contentsOf: pendingWhitespace) + result.append(piece) + pendingWhitespace.removeAll() + } } - // removes lines by skipping them from correctedLines - if Set(indexOfLinesToDelete).contains(currentLine.index) { - // reports every line that is being deleted - numberOfCorrections += 1 - continue // skips line + // Pull in any remaining pending whitespace + if !pendingWhitespace.isEmpty { + result.append(contentsOf: pendingWhitespace) } - // all lines that pass get added to final output file - correctedLines.append(currentLine.content) - } - // converts lines back to file and adds trailing line - if numberOfCorrections > 0 { - file.write(correctedLines.joined(separator: "\n") + "\n") + + return super.visit(token.with(\.leadingTrivia, Trivia(pieces: result))) } - return numberOfCorrections } } diff --git a/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift b/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift index 7f8589f401..f3fd7ad4e4 100644 --- a/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift +++ b/Tests/BuiltInRulesTests/VerticalWhitespaceRuleTests.swift @@ -9,7 +9,10 @@ final class VerticalWhitespaceRuleTests: SwiftLintTestCase { // Test with custom `max_empty_lines` let maxEmptyLinesDescription = VerticalWhitespaceRule.description .with(nonTriggeringExamples: [Example("let aaaa = 0\n\n\n")]) - .with(triggeringExamples: [Example("struct AAAA {}\n\n\n\n")]) + .with(triggeringExamples: [ + Example("struct AAAA {}\n\n\n\n"), + Example("class BBBB {\n \n \n \n}"), + ]) .with(corrections: [:]) verifyRule(maxEmptyLinesDescription, @@ -23,6 +26,7 @@ final class VerticalWhitespaceRuleTests: SwiftLintTestCase { .with(corrections: [ Example("let b = 0\n\n↓\n↓\n↓\n\nclass AAA {}\n"): Example("let b = 0\n\n\nclass AAA {}\n"), Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\n\nclass AAA {}\n"), + Example("class BB {\n \n \n↓ \n let b = 0\n}\n"): Example("class BB {\n \n \n let b = 0\n}\n"), ]) verifyRule(maxEmptyLinesDescription, From b6ebbcf404426013216f5fe5654c193d44b6f23f Mon Sep 17 00:00:00 2001 From: JP Simard Date: Thu, 19 Jun 2025 17:33:46 -0400 Subject: [PATCH 19/80] Format changelog (#6105) --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4794154854..dc8350b423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ ### Enhancements -* Fix false positives for `Actor`-conforming delegate protocols in the `class_delegate_protocol` rule. +* Fix false positives for `Actor`-conforming delegate protocols in the + `class_delegate_protocol` rule. [imsonalbajaj](https://github.com/imsonalbajaj) [#6054](https://github.com/realm/SwiftLint/issues/6054) @@ -22,7 +23,7 @@ and fewer false positives. [JP Simard](https://github.com/jpsim) -* Migrate `vertical_whitespace` rule from SourceKit to SwiftSyntax for improved performance. +* Migrate `vertical_whitespace` rule from SourceKit to SwiftSyntax for improved performance. [Matt Pennig](https://github.com/pennig) ### Bug Fixes From 3c2f4e31c9289a317f0e0692b9c3a4e53e6178cc Mon Sep 17 00:00:00 2001 From: JP Simard Date: Thu, 19 Jun 2025 18:20:17 -0400 Subject: [PATCH 20/80] Shard GeneratedTests into parallel targets and refactor code generation (#6102) Split the monolithic GeneratedTests target (242 test classes) into 10 sharded targets with ~25 tests each to enable parallel test execution. Reduces test time from 85.4s to 36.7s (57% improvement) by running shards concurrently. Most shards finish in 2-8s with 2 outliers at 30-37s. The implementation automatically scales with new rules and provides parallel test execution with improved code maintainability. --- Source/swiftlint-dev/Rules+Register.swift | 158 +- Tests/BUILD | 19 +- Tests/GeneratedTests/GeneratedTests.swift | 1460 ------------------ Tests/GeneratedTests/GeneratedTests_01.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_02.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_03.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_04.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_05.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_06.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_07.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_08.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_09.swift | 159 ++ Tests/GeneratedTests/GeneratedTests_10.swift | 111 ++ Tests/generated_tests.bzl | 21 + Tests/test_macros.bzl | 27 + 15 files changed, 1724 insertions(+), 1503 deletions(-) delete mode 100644 Tests/GeneratedTests/GeneratedTests.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_01.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_02.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_03.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_04.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_05.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_06.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_07.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_08.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_09.swift create mode 100644 Tests/GeneratedTests/GeneratedTests_10.swift create mode 100644 Tests/generated_tests.bzl create mode 100644 Tests/test_macros.bzl diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index 2507e27b2e..30e3d9eac5 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -47,6 +47,7 @@ extension SwiftLintDev.Rules { .sorted() try registerInRulesList(rules) try registerInTests(rules) + try registerInTestsBzl(rules) try registerInTestReference(adding: newRule) print("(Re-)Registered \(rules.count) rules.") } @@ -74,53 +75,158 @@ struct NewRuleDetails: Equatable { } } +/// Struct to hold processed rule information and shard calculations +private struct ProcessedRulesContext { + let baseRuleNames: [String] + let totalShards: Int + let shardSize: Int + + init(ruleFiles: [String], shardSize: Int) { + self.baseRuleNames = ruleFiles.map { $0.replacingOccurrences(of: ".swift", with: "") } + self.shardSize = shardSize + guard shardSize > 0, !baseRuleNames.isEmpty else { + self.totalShards = baseRuleNames.isEmpty ? 0 : 1 + return + } + self.totalShards = (baseRuleNames.count + shardSize - 1) / shardSize // Ceiling division + } + + /// Returns the rule names for a specific shard index + func shardRules(forIndex shardIndex: Int) -> ArraySlice { + let startIndex = shardIndex * shardSize + let endIndex = min(startIndex + shardSize, baseRuleNames.count) + return baseRuleNames[startIndex.. String { + """ + // GENERATED FILE. DO NOT EDIT! + + /// The rule list containing all available rules built into SwiftLint. + public let builtInRules: [any Rule.Type] = [ + \(rulesImportList.indent(by: 4)), + ] + + """ + } + + /// Generate content for Swift test files + private func generateSwiftTestFileContent(forTestClasses testClassesString: String) -> String { + """ + // GENERATED FILE. DO NOT EDIT! + // swiftlint:disable:previous file_name + // swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command + // swiftlint:disable file_length single_test_class type_name + + @testable import SwiftLintBuiltInRules + @testable import SwiftLintCore + import TestHelpers + + \(testClassesString) + + """ + } + + /// Generate content for Bazel .bzl files + private func generateBzlFileContent(macroInvocations: String) -> String { + #""" + # GENERATED FILE. DO NOT EDIT! + + load(":test_macros.bzl", "generated_test_shard") + + def generated_tests(copts, strict_concurrency_copts): + """Creates all generated test targets for SwiftLint rules. + + Args: + copts: Common compiler options + strict_concurrency_copts: Strict concurrency compiler options + """ + \#(macroInvocations) + + """# + } + func registerInRulesList(_ ruleFiles: [String]) throws { - let rules = ruleFiles + let rulesImportString = ruleFiles .map { $0.replacingOccurrences(of: ".swift", with: ".self") } .joined(separator: ",\n") let builtInRulesFile = rulesDirectory.deletingLastPathComponent() .appendingPathComponent("Models", isDirectory: true) .appendingPathComponent("BuiltInRules.swift", isDirectory: false) - try """ - // GENERATED FILE. DO NOT EDIT! - /// The rule list containing all available rules built into SwiftLint. - public let builtInRules: [any Rule.Type] = [ - \(rules.indent(by: 4)), - ] - - """.write(to: builtInRulesFile, atomically: true, encoding: .utf8) + let fileContent = generateBuiltInRulesFileContent(rulesImportList: rulesImportString) + try fileContent.write(to: builtInRulesFile, atomically: true, encoding: .utf8) } func registerInTests(_ ruleFiles: [String]) throws { - let testFile = testsDirectory - .appendingPathComponent("GeneratedTests.swift", isDirectory: false) - let rules = ruleFiles - .map { $0.replacingOccurrences(of: ".swift", with: "") } - .map { testName in """ + let rulesContext = ProcessedRulesContext(ruleFiles: ruleFiles, shardSize: Self.shardSize) + + // Remove old generated files + let existingFiles = try FileManager.default.contentsOfDirectory( + at: testsDirectory, + includingPropertiesForKeys: nil + ) + for file in existingFiles where file.lastPathComponent.hasPrefix("GeneratedTests") && + file.pathExtension == "swift" { + try FileManager.default.removeItem(at: file) + } + + // Create sharded test files + for shardIndex in 0.. Date: Thu, 19 Jun 2025 20:36:12 -0400 Subject: [PATCH 21/80] Add `build --incompatible_strict_action_env` to `.bazelrc` (#6106) Which should help get better remote cache hits. --- .bazelrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.bazelrc b/.bazelrc index 12ae6e95c6..0304307c3c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -13,6 +13,7 @@ build --disk_cache=~/.bazel_cache build --experimental_remote_cache_compression build --remote_build_event_upload=minimal build --nolegacy_important_outputs +build --incompatible_strict_action_env build:release \ --compilation_mode=opt \ From 775174b03a7f0dc1ebacfba3a28c9937d6897d35 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Thu, 19 Jun 2025 21:42:00 -0400 Subject: [PATCH 22/80] [CI] Use Bazel for "Register" job (#6110) Which should be much faster than the previous SwiftPM build in the common case of not having to rebuild SwiftSyntax. --- .buildkite/pipeline.yml | 2 +- Makefile | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 826d816d63..d887fc11a2 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -28,6 +28,6 @@ steps: - label: "Registration" commands: - echo "+++ Register Rules and Reporters" - - make --always-make register + - make --always-make bazel_register - echo "+++ Diff Files" - git diff --quiet HEAD diff --git a/Makefile b/Makefile index f8d5c8c425..ecb313108c 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,11 @@ register: swift run swiftlint-dev rules register swift run swiftlint-dev reporters register +bazel_register: + bazel build //:swiftlint-dev + ./bazel-bin/swiftlint-dev rules register + ./bazel-bin/swiftlint-dev reporters register + test: clean_xcode $(BUILD_TOOL) $(XCODEFLAGS) test From 614c0026dfb0eb06e94ed5ffbde9821599acc453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 20 Jun 2025 11:25:59 +0200 Subject: [PATCH 23/80] Prepare for Swift 6.2 (#6115) --- Source/SwiftLintCore/Models/SwiftVersion.swift | 2 ++ Tests/FrameworkTests/SwiftVersionTests.swift | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/SwiftLintCore/Models/SwiftVersion.swift b/Source/SwiftLintCore/Models/SwiftVersion.swift index 5082c9d337..920b42ff8d 100644 --- a/Source/SwiftLintCore/Models/SwiftVersion.swift +++ b/Source/SwiftLintCore/Models/SwiftVersion.swift @@ -73,6 +73,8 @@ public extension SwiftVersion { static let sixDotOneDotOne = SwiftVersion(rawValue: "6.1.1") /// Swift 6.1.2 static let sixDotOneDotTwo = SwiftVersion(rawValue: "6.1.2") + /// Swift 6.2 + static let sixDotTwo = SwiftVersion(rawValue: "6.2.0") /// The current detected Swift compiler version, based on the currently accessible SourceKit version. /// diff --git a/Tests/FrameworkTests/SwiftVersionTests.swift b/Tests/FrameworkTests/SwiftVersionTests.swift index e832e73777..30575af129 100644 --- a/Tests/FrameworkTests/SwiftVersionTests.swift +++ b/Tests/FrameworkTests/SwiftVersionTests.swift @@ -3,7 +3,9 @@ import XCTest final class SwiftVersionTests: SwiftLintTestCase { func testDetectSwiftVersion() { -#if compiler(>=6.1.2) +#if compiler(>=6.2) + let version = "6.2" +#elseif compiler(>=6.1.2) let version = "6.1.2" #elseif compiler(>=6.1.1) let version = "6.1.1" From 873b2b66e61883f4d48bb0aef228d42dd169b23a Mon Sep 17 00:00:00 2001 From: JP Simard Date: Fri, 20 Jun 2025 07:12:38 -0400 Subject: [PATCH 24/80] Require macOS 13 (#6114) --- BUILD | 2 +- CHANGELOG.md | 3 +++ Package.swift | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/BUILD b/BUILD index 6176354af0..a9466f09d5 100644 --- a/BUILD +++ b/BUILD @@ -190,7 +190,7 @@ apple_universal_binary( "x86_64", "arm64", ], - minimum_os_version = "12.0", + minimum_os_version = "13.0", platform_type = "macos", visibility = ["//visibility:public"], ) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8350b423..020b8889f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Use the severity levels `off`, `warning` or `error` instead. [kaseken](https://github.com/kaseken) +* SwiftLint now requires macOS 13 or higher to run. + [JP Simard](https://github.com/jpsim) + ### Experimental * None. diff --git a/Package.swift b/Package.swift index fabb7d323a..2f7dc042db 100644 --- a/Package.swift +++ b/Package.swift @@ -24,7 +24,7 @@ swiftLintPluginDependencies = [.target(name: "swiftlint")] let package = Package( name: "SwiftLint", - platforms: [.macOS(.v12)], + platforms: [.macOS(.v13)], products: [ .executable(name: "swiftlint", targets: ["swiftlint"]), .library(name: "SwiftLintFramework", targets: ["SwiftLintFramework"]), From 81474e36d0127b14bc424a0ca9dae3c410ed431a Mon Sep 17 00:00:00 2001 From: JP Simard Date: Fri, 20 Jun 2025 07:25:01 -0400 Subject: [PATCH 25/80] Enforce SourceKitFreeRule contract with fatal error (#6107) --- .../Rules/Lint/UnusedDeclarationRule.swift | 3 +- .../Extensions/Request+SwiftLint.swift | 29 ++++++++ Source/SwiftLintCore/Models/CurrentRule.swift | 11 +++ .../SwiftLintCore/Models/SwiftVersion.swift | 6 +- Source/SwiftLintFramework/Models/Linter.swift | 72 +++++++++++++------ .../FileHeaderRuleTests.swift | 6 ++ .../FrameworkTests/CollectingRuleTests.swift | 2 +- Tests/FrameworkTests/CustomRulesTests.swift | 6 ++ Tests/FrameworkTests/EmptyFileTests.swift | 2 +- .../FrameworkTests/SourceKitCrashTests.swift | 6 ++ 10 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 Source/SwiftLintCore/Models/CurrentRule.swift diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift index 616725f30a..cafd96f719 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift @@ -84,8 +84,7 @@ private extension SwiftLintFile { func index(compilerArguments: [String]) -> SourceKittenDictionary? { path .flatMap { path in - try? Request.index(file: path, arguments: compilerArguments) - .send() + try? Request.index(file: path, arguments: compilerArguments).send() } .map(SourceKittenDictionary.init) } diff --git a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift index 34acd32dc4..5d61b2750e 100644 --- a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift @@ -5,6 +5,35 @@ public extension Request { static let disableSourceKit = ProcessInfo.processInfo.environment["SWIFTLINT_DISABLE_SOURCEKIT"] != nil func sendIfNotDisabled() throws -> [String: any SourceKitRepresentable] { + // Skip safety checks if explicitly allowed (e.g., for testing or specific operations) + if !CurrentRule.allowSourceKitRequestWithoutRule { + // Check if we have a rule context + if let ruleID = CurrentRule.identifier { + // Skip registry check for mock test rules + if ruleID != "mock_test_rule_for_swiftlint_tests" { + // Ensure the rule exists in the registry + guard let ruleType = RuleRegistry.shared.rule(forID: ruleID) else { + queuedFatalError(""" + Rule '\(ruleID)' not found in RuleRegistry. This indicates a configuration or wiring issue. + """) + } + + // Check if the current rule is a SourceKitFreeRule + if ruleType is any SourceKitFreeRule.Type { + queuedFatalError(""" + '\(ruleID)' is a SourceKitFreeRule and should not be making requests to SourceKit. + """) + } + } + } else { + // No rule context and not explicitly allowed + queuedFatalError(""" + SourceKit request made outside of rule execution context without explicit permission. + Use CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { ... } for allowed exceptions. + """) + } + } + guard !Self.disableSourceKit else { throw Self.Error.connectionInterrupted("SourceKit is disabled by `SWIFTLINT_DISABLE_SOURCEKIT`.") } diff --git a/Source/SwiftLintCore/Models/CurrentRule.swift b/Source/SwiftLintCore/Models/CurrentRule.swift new file mode 100644 index 0000000000..81cd169d0c --- /dev/null +++ b/Source/SwiftLintCore/Models/CurrentRule.swift @@ -0,0 +1,11 @@ +/// A task-local value that holds the identifier of the currently executing rule. +/// This allows SourceKit request handling to determine if the current rule +/// is a SourceKitFreeRule without modifying function signatures throughout the codebase. +public enum CurrentRule { + /// The Rule ID for the currently executing rule. + @TaskLocal public static var identifier: String? + + /// Allows specific SourceKit requests to be made outside of rule execution context. + /// This should only be used for essential operations like getting the Swift version. + @TaskLocal public static var allowSourceKitRequestWithoutRule = false +} diff --git a/Source/SwiftLintCore/Models/SwiftVersion.swift b/Source/SwiftLintCore/Models/SwiftVersion.swift index 920b42ff8d..70da9f5d19 100644 --- a/Source/SwiftLintCore/Models/SwiftVersion.swift +++ b/Source/SwiftLintCore/Models/SwiftVersion.swift @@ -87,7 +87,11 @@ public extension SwiftVersion { if !Request.disableSourceKit { // This request was added in Swift 5.1 let params: SourceKitObject = ["key.request": UID("source.request.compiler_version")] - if let result = try? Request.customRequest(request: params).send(), + // Allow this specific SourceKit request outside of rule execution context + let result = CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + try? Request.customRequest(request: params).sendIfNotDisabled() + } + if let result, let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch { return SwiftVersion(rawValue: "\(major).\(minor).\(patch)") } diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index 67c1251b75..41cfd64901 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -1,5 +1,6 @@ import Foundation import SourceKittenFramework +import SwiftLintCore // swiftlint:disable file_length @@ -95,6 +96,32 @@ private extension Rule { compilerArguments: [String]) -> LintResult { let ruleID = Self.identifier + // Wrap entire lint process including shouldRun check in rule context + return CurrentRule.$identifier.withValue(ruleID) { + guard shouldRun(onFile: file) else { + return LintResult(violations: [], ruleTime: nil, deprecatedToValidIDPairs: []) + } + + return performLint( + file: file, + regions: regions, + benchmark: benchmark, + storage: storage, + superfluousDisableCommandRule: superfluousDisableCommandRule, + compilerArguments: compilerArguments + ) + } + } + + // swiftlint:disable:next function_parameter_count + private func performLint(file: SwiftLintFile, + regions: [Region], + benchmark: Bool, + storage: RuleStorage, + superfluousDisableCommandRule: SuperfluousDisableCommandRule?, + compilerArguments: [String]) -> LintResult { + let ruleID = Self.identifier + let violations: [StyleViolation] let ruleTime: (String, Double)? if benchmark { @@ -116,11 +143,11 @@ private extension Rule { } let customRulesIDs: [String] = { - guard let customRules = self as? CustomRules else { - return [] - } - return customRules.customRuleIdentifiers - }() + guard let customRules = self as? CustomRules else { + return [] + } + return customRules.customRuleIdentifiers + }() let ruleIDs = Self.description.allIdentifiers + customRulesIDs + (superfluousDisableCommandRule.map({ type(of: $0) })?.description.allIdentifiers ?? []) + @@ -247,7 +274,11 @@ public struct Linter { /// - returns: A linter capable of checking for violations after running each rule's collection step. public func collect(into storage: RuleStorage) -> CollectedLinter { DispatchQueue.concurrentPerform(iterations: rules.count) { idx in - rules[idx].collectInfo(for: file, into: storage, compilerArguments: compilerArguments) + let rule = rules[idx] + let ruleID = type(of: rule).identifier + CurrentRule.$identifier.withValue(ruleID) { + rule.collectInfo(for: file, into: storage, compilerArguments: compilerArguments) + } } return CollectedLinter(from: self) } @@ -306,15 +337,11 @@ public struct CollectedLinter { let superfluousDisableCommandRule = rules.first(where: { $0 is SuperfluousDisableCommandRule }) as? SuperfluousDisableCommandRule - let validationResults: [LintResult] = rules.parallelCompactMap { - guard $0.shouldRun(onFile: file) else { - return nil - } - - return $0.lint(file: file, regions: regions, benchmark: benchmark, - storage: storage, - superfluousDisableCommandRule: superfluousDisableCommandRule, - compilerArguments: compilerArguments) + let validationResults: [LintResult] = rules.parallelMap { + $0.lint(file: file, regions: regions, benchmark: benchmark, + storage: storage, + superfluousDisableCommandRule: superfluousDisableCommandRule, + compilerArguments: compilerArguments) } let undefinedSuperfluousCommandViolations = self.undefinedSuperfluousCommandViolations( regions: regions, configuration: configuration, @@ -381,17 +408,20 @@ public struct CollectedLinter { } var corrections = [String: Int]() - for rule in rules where rule.shouldRun(onFile: file) { - guard let rule = rule as? any CorrectableRule else { - continue + for rule in rules.compactMap({ $0 as? any CorrectableRule }) { + // Set rule context before checking shouldRun to allow file property access + let ruleCorrections = CurrentRule.$identifier.withValue(type(of: rule).identifier) { () -> Int? in + guard rule.shouldRun(onFile: file) else { + return nil + } + return rule.correct(file: file, using: storage, compilerArguments: compilerArguments) } - let corrected = rule.correct(file: file, using: storage, compilerArguments: compilerArguments) - if corrected != 0 { + if let corrected = ruleCorrections, corrected != 0 { corrections[type(of: rule).description.identifier] = corrected if !file.isVirtual { file.invalidateCache() } - } + } } return corrections } diff --git a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift index 82b56cdfea..d60354b42e 100644 --- a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift @@ -5,6 +5,12 @@ import XCTest private let fixturesDirectory = "\(TestResources.path())/FileHeaderRuleFixtures" final class FileHeaderRuleTests: SwiftLintTestCase { + override func invokeTest() { + CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + super.invokeTest() + } + } + private func validate(fileName: String, using configuration: Any) throws -> [StyleViolation] { let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! let rule = try FileHeaderRule(configuration: configuration) diff --git a/Tests/FrameworkTests/CollectingRuleTests.swift b/Tests/FrameworkTests/CollectingRuleTests.swift index 04a11a39e3..7e87dc3440 100644 --- a/Tests/FrameworkTests/CollectingRuleTests.swift +++ b/Tests/FrameworkTests/CollectingRuleTests.swift @@ -136,7 +136,7 @@ extension MockCollectingRule { @RuleConfigurationDescriptionBuilder var configurationDescription: some Documentable { RuleConfigurationOption.noOptions } static var description: RuleDescription { - RuleDescription(identifier: "test_rule", name: "", description: "", kind: .lint) + RuleDescription(identifier: "mock_test_rule_for_swiftlint_tests", name: "", description: "", kind: .lint) } static var configuration: Configuration? { Configuration(rulesMode: .onlyConfiguration([identifier]), ruleList: RuleList(rules: self)) diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 87691f4e79..efbfa43f5a 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -11,6 +11,12 @@ final class CustomRulesTests: SwiftLintTestCase { private var testFile: SwiftLintFile { SwiftLintFile(path: "\(TestResources.path())/test.txt")! } + override func invokeTest() { + CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + super.invokeTest() + } + } + func testCustomRuleConfigurationSetsCorrectlyWithMatchKinds() { let configDict = [ "my_custom_rule": [ diff --git a/Tests/FrameworkTests/EmptyFileTests.swift b/Tests/FrameworkTests/EmptyFileTests.swift index 4fe7c61404..feee49af3f 100644 --- a/Tests/FrameworkTests/EmptyFileTests.swift +++ b/Tests/FrameworkTests/EmptyFileTests.swift @@ -39,7 +39,7 @@ private struct DontLintEmptyFiles: ShouldLintEmptyFilesProtocol { static var shouldLintEmptyFiles: Bool { false } } -private struct RuleMock: CorrectableRule { +private struct RuleMock: CorrectableRule, SourceKitFreeRule { var configuration = SeverityConfiguration(.warning) static var description: RuleDescription { diff --git a/Tests/FrameworkTests/SourceKitCrashTests.swift b/Tests/FrameworkTests/SourceKitCrashTests.swift index fbb429c92b..553aaaf025 100644 --- a/Tests/FrameworkTests/SourceKitCrashTests.swift +++ b/Tests/FrameworkTests/SourceKitCrashTests.swift @@ -2,6 +2,12 @@ import XCTest final class SourceKitCrashTests: SwiftLintTestCase { + override func invokeTest() { + CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { + super.invokeTest() + } + } + func testAssertHandlerIsNotCalledOnNormalFile() { let file = SwiftLintFile(contents: "A file didn't crash SourceKitService") file.sourcekitdFailed = false From 7e0ea598ceeba4af261f8dc8781d7bf09d32e533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 21 Jun 2025 00:23:29 +0200 Subject: [PATCH 26/80] Let `large_tuple` rule adhere to common style (#6120) --- .../Rules/Metrics/LargeTupleRule.swift | 93 ++++++++++++++----- .../Metrics/LargeTupleRuleExamples.swift | 56 ----------- 2 files changed, 68 insertions(+), 81 deletions(-) delete mode 100644 Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift index b3196aefc8..e54ad3f384 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift @@ -1,6 +1,7 @@ import SwiftSyntax -struct LargeTupleRule: SourceKitFreeRule { +@SwiftSyntaxRule +struct LargeTupleRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 2, error: 3) static let description = RuleDescription( @@ -8,32 +9,74 @@ struct LargeTupleRule: SourceKitFreeRule { name: "Large Tuple", description: "Tuples shouldn't have too many members. Create a custom type instead.", kind: .metrics, - nonTriggeringExamples: LargeTupleRuleExamples.nonTriggeringExamples, - triggeringExamples: LargeTupleRuleExamples.triggeringExamples + nonTriggeringExamples: [ + Example("let foo: (Int, Int)"), + Example("let foo: (start: Int, end: Int)"), + Example("let foo: (Int, (Int, String))"), + Example("func foo() -> (Int, Int)"), + Example("func foo() -> (Int, Int) {}"), + Example("func foo(bar: String) -> (Int, Int)"), + Example("func foo(bar: String) -> (Int, Int) {}"), + Example("func foo() throws -> (Int, Int)"), + Example("func foo() throws -> (Int, Int) {}"), + Example("let foo: (Int, Int, Int) -> Void"), + Example("let foo: (Int, Int, Int) throws -> Void"), + Example("func foo(bar: (Int, String, Float) -> Void)"), + Example("func foo(bar: (Int, String, Float) throws -> Void)"), + Example("var completionHandler: ((_ data: Data?, _ resp: URLResponse?, _ e: NSError?) -> Void)!"), + Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), + Example("func getGenericTypeAndInt() -> (Type, Int)?"), + Example("func foo() async -> (Int, Int)"), + Example("func foo() async -> (Int, Int) {}"), + Example("func foo(bar: String) async -> (Int, Int)"), + Example("func foo(bar: String) async -> (Int, Int) {}"), + Example("func foo() async throws -> (Int, Int)"), + Example("func foo() async throws -> (Int, Int) {}"), + Example("let foo: (Int, Int, Int) async -> Void"), + Example("let foo: (Int, Int, Int) async throws -> Void"), + Example("func foo(bar: (Int, String, Float) async -> Void)"), + Example("func foo(bar: (Int, String, Float) async throws -> Void)"), + Example("func getDictionaryAndInt() async -> (Dictionary, Int)?"), + Example("func getGenericTypeAndInt() async -> (Type, Int)?"), + ], + triggeringExamples: [ + Example("let foo: ↓(Int, Int, Int)"), + Example("let foo: ↓(start: Int, end: Int, value: String)"), + Example("let foo: (Int, ↓(Int, Int, Int))"), + Example("func foo(bar: ↓(Int, Int, Int))"), + Example("func foo() -> ↓(Int, Int, Int)"), + Example("func foo() -> ↓(Int, Int, Int) {}"), + Example("func foo(bar: String) -> ↓(Int, Int, Int)"), + Example("func foo(bar: String) -> ↓(Int, Int, Int) {}"), + Example("func foo() throws -> ↓(Int, Int, Int)"), + Example("func foo() throws -> ↓(Int, Int, Int) {}"), + Example("func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}"), + Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), + Example("func foo(bar: ↓(Int, Int, Int)) async"), + Example("func foo() async -> ↓(Int, Int, Int)"), + Example("func foo() async -> ↓(Int, Int, Int) {}"), + Example("func foo(bar: String) async -> ↓(Int, Int, Int)"), + Example("func foo(bar: String) async -> ↓(Int, Int, Int) {}"), + Example("func foo() async throws -> ↓(Int, Int, Int)"), + Example("func foo() async throws -> ↓(Int, Int, Int) {}"), + Example("func foo() async throws -> ↓(Int, ↓(String, String, String), Int) {}"), + Example("func getDictionaryAndInt() async -> (Dictionary, Int)?"), + ] ) - - func validate(file: SwiftLintFile) -> [StyleViolation] { - LargeTupleRuleVisitor(viewMode: .sourceAccurate) - .walk(file: file, handler: \.violationPositions) - .sorted(by: { $0.position < $1.position }) - .compactMap { position, size in - for parameter in configuration.params where size > parameter.value { - let reason = "Tuples should have at most \(configuration.warning) members" - return StyleViolation(ruleDescription: Self.description, - severity: parameter.severity, - location: Location(file: file, position: position), - reason: reason) - } - - return nil - } - } } -private final class LargeTupleRuleVisitor: SyntaxVisitor { - private(set) var violationPositions: [(position: AbsolutePosition, memberCount: Int)] = [] - - override func visitPost(_ node: TupleTypeSyntax) { - violationPositions.append((node.positionAfterSkippingLeadingTrivia, node.elements.count)) +private extension LargeTupleRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: TupleTypeSyntax) { + let memberCount = node.elements.count + for parameter in configuration.params where memberCount > parameter.value { + violations.append(.init( + position: node.positionAfterSkippingLeadingTrivia, + reason: "Tuples should have at most \(parameter.value) members", + severity: parameter.severity + )) + return + } + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift deleted file mode 100644 index ec604610ee..0000000000 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRuleExamples.swift +++ /dev/null @@ -1,56 +0,0 @@ -struct LargeTupleRuleExamples { - static let nonTriggeringExamples: [Example] = [ - Example("let foo: (Int, Int)"), - Example("let foo: (start: Int, end: Int)"), - Example("let foo: (Int, (Int, String))"), - Example("func foo() -> (Int, Int)"), - Example("func foo() -> (Int, Int) {}"), - Example("func foo(bar: String) -> (Int, Int)"), - Example("func foo(bar: String) -> (Int, Int) {}"), - Example("func foo() throws -> (Int, Int)"), - Example("func foo() throws -> (Int, Int) {}"), - Example("let foo: (Int, Int, Int) -> Void"), - Example("let foo: (Int, Int, Int) throws -> Void"), - Example("func foo(bar: (Int, String, Float) -> Void)"), - Example("func foo(bar: (Int, String, Float) throws -> Void)"), - Example("var completionHandler: ((_ data: Data?, _ resp: URLResponse?, _ e: NSError?) -> Void)!"), - Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), - Example("func getGenericTypeAndInt() -> (Type, Int)?"), - Example("func foo() async -> (Int, Int)"), - Example("func foo() async -> (Int, Int) {}"), - Example("func foo(bar: String) async -> (Int, Int)"), - Example("func foo(bar: String) async -> (Int, Int) {}"), - Example("func foo() async throws -> (Int, Int)"), - Example("func foo() async throws -> (Int, Int) {}"), - Example("let foo: (Int, Int, Int) async -> Void"), - Example("let foo: (Int, Int, Int) async throws -> Void"), - Example("func foo(bar: (Int, String, Float) async -> Void)"), - Example("func foo(bar: (Int, String, Float) async throws -> Void)"), - Example("func getDictionaryAndInt() async -> (Dictionary, Int)?"), - Example("func getGenericTypeAndInt() async -> (Type, Int)?"), - ] - - static let triggeringExamples: [Example] = [ - Example("let foo: ↓(Int, Int, Int)"), - Example("let foo: ↓(start: Int, end: Int, value: String)"), - Example("let foo: (Int, ↓(Int, Int, Int))"), - Example("func foo(bar: ↓(Int, Int, Int))"), - Example("func foo() -> ↓(Int, Int, Int)"), - Example("func foo() -> ↓(Int, Int, Int) {}"), - Example("func foo(bar: String) -> ↓(Int, Int, Int)"), - Example("func foo(bar: String) -> ↓(Int, Int, Int) {}"), - Example("func foo() throws -> ↓(Int, Int, Int)"), - Example("func foo() throws -> ↓(Int, Int, Int) {}"), - Example("func foo() throws -> ↓(Int, ↓(String, String, String), Int) {}"), - Example("func getDictionaryAndInt() -> (Dictionary, Int)?"), - Example("func foo(bar: ↓(Int, Int, Int)) async"), - Example("func foo() async -> ↓(Int, Int, Int)"), - Example("func foo() async -> ↓(Int, Int, Int) {}"), - Example("func foo(bar: String) async -> ↓(Int, Int, Int)"), - Example("func foo(bar: String) async -> ↓(Int, Int, Int) {}"), - Example("func foo() async throws -> ↓(Int, Int, Int)"), - Example("func foo() async throws -> ↓(Int, Int, Int) {}"), - Example("func foo() async throws -> ↓(Int, ↓(String, String, String), Int) {}"), - Example("func getDictionaryAndInt() async -> (Dictionary, Int)?"), - ] -} From 573ac4da7d93172aa3809ffa9abeac4e3d509aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 21 Jun 2025 01:40:06 +0200 Subject: [PATCH 27/80] Report warning threshold (#6122) --- Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift index e54ad3f384..35d31abd8d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LargeTupleRule.swift @@ -72,7 +72,7 @@ private extension LargeTupleRule { for parameter in configuration.params where memberCount > parameter.value { violations.append(.init( position: node.positionAfterSkippingLeadingTrivia, - reason: "Tuples should have at most \(parameter.value) members", + reason: "Tuples should have at most \(configuration.warning) members", severity: parameter.severity )) return From 5a3c8c9ba3847ed0a1fcad55884133716758b16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 21 Jun 2025 14:02:28 +0200 Subject: [PATCH 28/80] Inline rule-specific parts of BodyLengthVisitor (#6121) Main goal is to bring implementations, rules and examples closer together. Another advantage is that the rule's layouts are in line with other `@SwiftSyntaxRule`s. After the refactoring, violation messages can be better adapted to the object causing the issue. Violation positions should be harmonized and more cases for protocols, subscripts and deinitializers can be added. --- .../Rules/Metrics/ClosureBodyLengthRule.swift | 19 ++- .../Metrics/FunctionBodyLengthRule.swift | 31 +++- .../Rules/Metrics/TypeBodyLengthRule.swift | 34 ++++- .../Visitors/BodyLengthRuleVisitor.swift | 142 ------------------ .../Visitors/BodyLengthVisitor.swift | 47 ++++++ 5 files changed, 120 insertions(+), 153 deletions(-) delete mode 100644 Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift create mode 100644 Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift index 5e322e5182..2ecb9ed825 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift @@ -1,5 +1,9 @@ -struct ClosureBodyLengthRule: OptInRule, SwiftSyntaxRule { +import SwiftSyntax + +@SwiftSyntaxRule(optIn: true) +struct ClosureBodyLengthRule: Rule { private static let defaultWarningThreshold = 30 + var configuration = SeverityLevelsConfiguration(warning: Self.defaultWarningThreshold, error: 100) static let description = RuleDescription( @@ -15,8 +19,17 @@ struct ClosureBodyLengthRule: OptInRule, SwiftSyntaxRule { nonTriggeringExamples: ClosureBodyLengthRuleExamples.nonTriggeringExamples, triggeringExamples: ClosureBodyLengthRuleExamples.triggeringExamples ) +} - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - BodyLengthRuleVisitor(kind: .closure, file: file, configuration: configuration) +private extension ClosureBodyLengthRule { + final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: ClosureExprSyntax) { + registerViolations( + leftBrace: node.leftBrace, + rightBrace: node.rightBrace, + violationNode: node.leftBrace, + objectName: "Closure" + ) + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index dc6eb5fe9f..a2de07b288 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -1,6 +1,7 @@ -import SwiftLintCore +import SwiftSyntax -struct FunctionBodyLengthRule: SwiftSyntaxRule { +@SwiftSyntaxRule +struct FunctionBodyLengthRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 50, error: 100) static let description = RuleDescription( @@ -9,8 +10,30 @@ struct FunctionBodyLengthRule: SwiftSyntaxRule { description: "Function bodies should not span too many lines", kind: .metrics ) +} + +private extension FunctionBodyLengthRule { + final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: FunctionDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.name, + objectName: "Function" + ) + } + } - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - BodyLengthRuleVisitor(kind: .function, file: file, configuration: configuration) + override func visitPost(_ node: InitializerDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.initKeyword, + objectName: "Function" + ) + } + } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index 08c1318a04..622ce03d65 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -1,4 +1,4 @@ -import SwiftLintCore +import SwiftSyntax private func wrapExample( prefix: String = "", @@ -12,7 +12,8 @@ private func wrapExample( repeatElement(template, count: count).joined() + "\(add)}\n", file: file, line: line) } -struct TypeBodyLengthRule: SwiftSyntaxRule { +@SwiftSyntaxRule +struct TypeBodyLengthRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 250, error: 350) static let description = RuleDescription( @@ -32,8 +33,33 @@ struct TypeBodyLengthRule: SwiftSyntaxRule { wrapExample(prefix: "↓", type, "let abc = 0\n", 251) }) ) +} + +private extension TypeBodyLengthRule { + final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: ActorDeclSyntax) { + collectViolation(node) + } + + override func visitPost(_ node: EnumDeclSyntax) { + collectViolation(node) + } + + override func visitPost(_ node: ClassDeclSyntax) { + collectViolation(node) + } + + override func visitPost(_ node: StructDeclSyntax) { + collectViolation(node) + } - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - BodyLengthRuleVisitor(kind: .type, file: file, configuration: configuration) + private func collectViolation(_ node: some DeclGroupSyntax) { + registerViolations( + leftBrace: node.memberBlock.leftBrace, + rightBrace: node.memberBlock.rightBrace, + violationNode: node.introducer, + objectName: "Type" + ) + } } } diff --git a/Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift b/Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift deleted file mode 100644 index 22e985a65a..0000000000 --- a/Source/SwiftLintCore/Visitors/BodyLengthRuleVisitor.swift +++ /dev/null @@ -1,142 +0,0 @@ -import SwiftSyntax - -/// Visitor that collection violations of code block lengths. -public final class BodyLengthRuleVisitor: ViolationsSyntaxVisitor> { - @usableFromInline let kind: Kind - - /// The code block types to check. - public enum Kind { - /// Closure code blocks. - case closure - /// Function body blocks. - case function - /// Type (class, enum, ...) member blocks. - case type - - fileprivate var name: String { - switch self { - case .closure: - return "Closure" - case .function: - return "Function" - case .type: - return "Type" - } - } - } - - /// Initializer. - /// - /// - Parameters: - /// - kind: The code block type to check. See ``Kind``. - /// - file: The file to collect violation for. - /// - configuration: The configuration that defines the acceptable limits. - @inlinable - public init(kind: Kind, file: SwiftLintFile, configuration: SeverityLevelsConfiguration) { - self.kind = kind - super.init(configuration: configuration, file: file) - } - - override public func visitPost(_ node: EnumDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.enumKeyword - ) - } - } - - override public func visitPost(_ node: ClassDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.classKeyword - ) - } - } - - override public func visitPost(_ node: StructDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.structKeyword - ) - } - } - - override public func visitPost(_ node: ActorDeclSyntax) { - if kind == .type { - registerViolations( - leftBrace: node.memberBlock.leftBrace, - rightBrace: node.memberBlock.rightBrace, - violationNode: node.actorKeyword - ) - } - } - - override public func visitPost(_ node: ClosureExprSyntax) { - if kind == .closure { - registerViolations( - leftBrace: node.leftBrace, - rightBrace: node.rightBrace, - violationNode: node.leftBrace - ) - } - } - - override public func visitPost(_ node: FunctionDeclSyntax) { - if kind == .function, let body = node.body { - registerViolations( - leftBrace: body.leftBrace, - rightBrace: body.rightBrace, - violationNode: node.name - ) - } - } - - override public func visitPost(_ node: InitializerDeclSyntax) { - if kind == .function, let body = node.body { - registerViolations( - leftBrace: body.leftBrace, - rightBrace: body.rightBrace, - violationNode: node.initKeyword - ) - } - } - - private func registerViolations( - leftBrace: TokenSyntax, rightBrace: TokenSyntax, violationNode: some SyntaxProtocol - ) { - let leftBracePosition = leftBrace.positionAfterSkippingLeadingTrivia - let leftBraceLine = locationConverter.location(for: leftBracePosition).line - let rightBracePosition = rightBrace.positionAfterSkippingLeadingTrivia - let rightBraceLine = locationConverter.location(for: rightBracePosition).line - let lineCount = file.bodyLineCountIgnoringCommentsAndWhitespace(leftBraceLine: leftBraceLine, - rightBraceLine: rightBraceLine) - let severity: ViolationSeverity, upperBound: Int - if let error = configuration.error, lineCount > error { - severity = .error - upperBound = error - } else if lineCount > configuration.warning { - severity = .warning - upperBound = configuration.warning - } else { - return - } - - let reason = """ - \(kind.name) body should span \(upperBound) lines or less excluding comments and whitespace: \ - currently spans \(lineCount) lines - """ - - let violation = ReasonedRuleViolation( - position: violationNode.positionAfterSkippingLeadingTrivia, - reason: reason, - severity: severity - ) - violations.append(violation) - } -} diff --git a/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift b/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift new file mode 100644 index 0000000000..d303599692 --- /dev/null +++ b/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift @@ -0,0 +1,47 @@ +import SwiftSyntax + +/// Violation visitor customized to collect violations of code blocks that exceed a specified number of lines. +open class BodyLengthVisitor: ViolationsSyntaxVisitor> { + @inlinable + override public init(configuration: SeverityLevelsConfiguration, file: SwiftLintFile) { + super.init(configuration: configuration, file: file) + } + + /// Registers a violation if a body exceeds the configured line count. + /// + /// - Parameters: + /// - leftBrace: The left brace token of the body. + /// - rightBrace: The right brace token of the body. + /// - violationNode: The syntax node where the violation is to be reported. + /// - objectName: The name of the object (e.g., "Function", "Closure") used in the violation message. + public func registerViolations(leftBrace: TokenSyntax, + rightBrace: TokenSyntax, + violationNode: some SyntaxProtocol, + objectName: String) { + let leftBracePosition = leftBrace.positionAfterSkippingLeadingTrivia + let leftBraceLine = locationConverter.location(for: leftBracePosition).line + let rightBracePosition = rightBrace.positionAfterSkippingLeadingTrivia + let rightBraceLine = locationConverter.location(for: rightBracePosition).line + let lineCount = file.bodyLineCountIgnoringCommentsAndWhitespace(leftBraceLine: leftBraceLine, + rightBraceLine: rightBraceLine) + let severity: ViolationSeverity, upperBound: Int + if let error = configuration.error, lineCount > error { + severity = .error + upperBound = error + } else if lineCount > configuration.warning { + severity = .warning + upperBound = configuration.warning + } else { + return + } + + violations.append(.init( + position: violationNode.positionAfterSkippingLeadingTrivia, + reason: """ + \(objectName) body should span \(upperBound) lines or less excluding comments and whitespace: \ + currently spans \(lineCount) lines + """, + severity: severity + )) + } +} From 5eac9be50dc2196bc29a425e6fa46518ffbedc80 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 10:11:43 -0400 Subject: [PATCH 29/80] Migrate AccessibilityTraitForButtonRule from SourceKit to SwiftSyntax (#6108) ## Summary Convert AccessibilityTraitForButtonRule to use SwiftSyntax instead of SourceKit for improved performance and better accessibility trait detection in SwiftUI modifier chains. ## Key Technical Improvements - **Bidirectional modifier chain analysis** for comprehensive accessibility trait detection - **Better context awareness** through SwiftSyntax tree traversal - **Accurate container exemption logic** for Button/Link components - **Enhanced gesture detection** supporting `.onTapGesture` and `.gesture` modifiers - **Improved performance** with SwiftSyntax visitor pattern over SourceKit AST parsing ## Migration Details - Replaced `ASTRule` with `@SwiftSyntaxRule(optIn: true)` annotation - Implemented `ViolationsSyntaxVisitor` pattern for systematic tree traversal - Added bidirectional accessibility trait detection that checks both before and after tap gestures - Enhanced exemption logic for inherently accessible containers (Button, NavigationLink) - Maintained full compatibility with existing rule behavior and test cases --- CHANGELOG.md | 8 + .../AccessibilityTraitForButtonRule.swift | 332 +++++++++++++----- 2 files changed, 245 insertions(+), 95 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 020b8889f7..cc58386732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,14 @@ * Migrate `vertical_whitespace` rule from SourceKit to SwiftSyntax for improved performance. [Matt Pennig](https://github.com/pennig) +* Migrate `accessibility_label_for_image` rule from SourceKit to SwiftSyntax for improved + performance and fewer false positives. + [JP Simard](https://github.com/jpsim) + +* Migrate `accessibility_trait_for_button` rule from SourceKit to SwiftSyntax for improved + performance and fewer false positives. + [JP Simard](https://github.com/jpsim) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift index 006783a2d3..11176fbbf2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift @@ -1,6 +1,7 @@ -import SourceKittenFramework +import SwiftSyntax -struct AccessibilityTraitForButtonRule: ASTRule, OptInRule { +@SwiftSyntaxRule(optIn: true) +struct AccessibilityTraitForButtonRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -25,119 +26,260 @@ struct AccessibilityTraitForButtonRule: ASTRule, OptInRule { nonTriggeringExamples: AccessibilityTraitForButtonRuleExamples.nonTriggeringExamples, triggeringExamples: AccessibilityTraitForButtonRuleExamples.triggeringExamples ) +} - // MARK: AST Rule +private extension AccessibilityTraitForButtonRule { + final class Visitor: ViolationsSyntaxVisitor { + private var isInViewStruct = false - func validate(file: SwiftLintFile, - kind: SwiftDeclarationKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - // Only proceed to check View structs. - guard kind == .struct, - dictionary.inheritedTypes.contains("View"), - dictionary.substructure.isNotEmpty else { - return [] + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + isInViewStruct = node.isViewStruct + return .visitChildren } - return findButtonTraitViolations(file: file, substructure: dictionary.substructure) - } + override func visitPost(_: StructDeclSyntax) { + isInViewStruct = false + } - /// Recursively check a file for image violations, and return all such violations. - private func findButtonTraitViolations( - file: SwiftLintFile, - substructure: [SourceKittenDictionary] - ) -> [StyleViolation] { - var violations = [StyleViolation]() - for dictionary in substructure { - guard let offset: ByteCount = dictionary.offset else { - continue + override func visitPost(_ node: FunctionCallExprSyntax) { + guard isInViewStruct, node.isSingleTapGestureModifier() else { + return } - // If it has a tap gesture and does not have a button or link trait, it's a violation. - // Also allowing ones that are hidden from accessibility, though that's not recommended. - if dictionary.hasOnSingleTapModifier(in: file) { - if dictionary.hasAccessibilityTrait(".isButton", in: file) || - dictionary.hasAccessibilityTrait(".isLink", in: file) || - dictionary.hasAccessibilityHiddenModifier(in: file) { - continue - } - + // The 'node' is the tap gesture modifier itself. + // Check if this node, any preceding modifiers in the chain, or an encapsulating Button/Link provide + // exemption. + if !AccessibilityButtonTraitDeterminator.isExempt(tapGestureNode: node) { violations.append( - StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: offset)) + ReasonedRuleViolation( + // Position of .onTapGesture etc. + position: node.calledExpression.positionAfterSkippingLeadingTrivia, + reason: AccessibilityTraitForButtonRule.description.description, + severity: configuration.severity + ) ) } + } + } +} + +private struct AccessibilityButtonTraitDeterminator { + static let maxSearchDepth = 20 // Limit for traversing up the syntax tree + + static func isExempt(tapGestureNode: FunctionCallExprSyntax) -> Bool { + // 1. Check if accessibility traits are present anywhere in the same modifier chain + if hasAccessibilityTraitsInChain(tapGestureNode: tapGestureNode) { + return true + } - // If dictionary did not represent a View with a tap gesture, recursively check substructure, - // unless it's a container that hides its children from accessibility. - else if dictionary.substructure.isNotEmpty { - if dictionary.hasAccessibilityHiddenModifier(in: file) || - dictionary.hasAccessibilityElementChildrenIgnoreModifier(in: file) { - continue + // 2. Check if the view (to which the gesture is applied) is part of an inherently exempting container + return isInsideInherentlyExemptingContainer(startingFrom: tapGestureNode) + } + + private static func hasAccessibilityTraitsInChain(tapGestureNode: FunctionCallExprSyntax) -> Bool { + // Check both directions: before the tap gesture (backwards in chain) and after (ancestors in tree) + + // 1. Check backwards in the modifier chain (modifiers applied before the tap gesture) + if hasAccessibilityTraitsBackwards(from: tapGestureNode) { + return true + } + + // 2. Check forwards by looking at parent nodes (modifiers applied after the tap gesture) + return hasAccessibilityTraitsForwards(from: tapGestureNode) + } + + private static func hasAccessibilityTraitsBackwards(from tapGestureNode: FunctionCallExprSyntax) -> Bool { + var current: ExprSyntax? = ExprSyntax(tapGestureNode) + var depth = 0 + + while let currentExpr = current, depth < maxSearchDepth { + defer { depth += 1 } + + if let funcCall = currentExpr.as(FunctionCallExprSyntax.self) { + // Check if this modifier provides accessibility traits + if funcCall.providesButtonOrLinkTrait() || funcCall.isAccessibilityHiddenTrue() { + return true } - violations.append( - contentsOf: findButtonTraitViolations(file: file, substructure: dictionary.substructure) - ) + // Move to the previous modifier in the chain (the base of the member access) + if let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) { + current = memberAccess.base + } else { + // Reached the end of the chain (e.g., Text(...)) + break + } + } else { + break + } + } + + return false + } + + private static func hasAccessibilityTraitsForwards(from tapGestureNode: FunctionCallExprSyntax) -> Bool { + // Look at parent nodes to see if accessibility traits are applied after the tap gesture + var currentNode: Syntax? = Syntax(tapGestureNode).parent + var depth = 0 + + while let node = currentNode, depth < maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + // Check if this parent node is a function call with accessibility traits + if let funcCall = node.as(FunctionCallExprSyntax.self) { + if funcCall.providesButtonOrLinkTrait() || funcCall.isAccessibilityHiddenTrue() { + return true + } + } + + // Stop at certain boundaries + if node.is(StmtSyntax.self) || node.is(StructDeclSyntax.self) { + break + } + } + + return false + } + + private static func isInsideInherentlyExemptingContainer(startingFrom node: FunctionCallExprSyntax) -> Bool { + var currentNode: Syntax? = Syntax(node) + var depth = 0 + + while let currentSyntaxNode = currentNode, depth < maxSearchDepth { + defer { + currentNode = currentSyntaxNode.parent + depth += 1 + } + + if let funcCall = currentSyntaxNode.as(FunctionCallExprSyntax.self), + let identifier = funcCall.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text, + ["Button", "Link"].contains(identifier) { + return true + } + + // Stop if we reach a new View declaration or similar boundary + if currentSyntaxNode.is(StructDeclSyntax.self) || + currentSyntaxNode.is(ClassDeclSyntax.self) || + currentSyntaxNode.is(EnumDeclSyntax.self) { + break } } + return false + } + + /// Helper to check if an expression (part of a gesture modifier argument) is TapGesture(count: 1) or TapGesture() + fileprivate static func isSingleTapGestureInstance(expression: ExprSyntax) -> Bool { + var currentExpr: ExprSyntax? = expression - return violations + // Traverse down if it's a chain of gesture modifiers like .onEnded to find the base gesture. + while let memberCall = currentExpr?.as(FunctionCallExprSyntax.self), // e.g. TapGesture().onEnded() + let memberAccess = memberCall.calledExpression.as(MemberAccessExprSyntax.self), + memberAccess.base != nil { // Ensure it's a chain like x.method() + currentExpr = memberAccess.base + } + + guard let gestureCall = currentExpr?.as(FunctionCallExprSyntax.self), + let gestureName = gestureCall.calledExpression.as(DeclReferenceExprSyntax.self)?.baseName.text, + gestureName == "TapGesture" else { + return false + } + + // Check count argument: TapGesture() or TapGesture(count: 1) + if gestureCall.arguments.isEmpty { return true } // Default count is 1 + + if let countArg = gestureCall.arguments.first(where: { $0.label?.text == "count" }) { + return countArg.expression.as(IntegerLiteralExprSyntax.self)?.literal.text == "1" + } + + // If 'count' is not specified, it defaults to 1. + return !gestureCall.arguments.contains(where: { $0.label?.text == "count" }) + } +} + +private extension StructDeclSyntax { + var isViewStruct: Bool { + guard let inheritanceClause else { return false } + return inheritanceClause.inheritedTypes.contains { inheritedType in + inheritedType.type.as(IdentifierTypeSyntax.self)?.name.text == "View" + } } } -// MARK: SourceKittenDictionary extensions - -private extension SourceKittenDictionary { - /// Whether or not the dictionary represents a SwiftUI View with a tap gesture where the `count` argument is 1. - /// A single tap can be represented by an `onTapGesture` modifier with a count of 1 (default value is 1), - /// or by a `gesture`, `simultaneousGesture`, or `highPriorityGesture` modifier with an argument - /// starting with a `TapGesture` object with a count of 1 (default value is 1). - func hasOnSingleTapModifier(in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "onTapGesture", - arguments: [.init(name: "count", required: false, values: ["1"])] - ), - SwiftUIModifier( - name: "gesture", - arguments: [ - .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) - ] - ), - SwiftUIModifier( - name: "simultaneousGesture", - arguments: [ - .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) - ] - ), - SwiftUIModifier( - name: "highPriorityGesture", - arguments: [ - .init(name: "", values: ["TapGesture()", "TapGesture(count: 1)"], matchType: .prefix) - ] - ), - ], - in: file - ) +private extension FunctionCallExprSyntax { + func isSingleTapGestureModifier() -> Bool { + guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false } + let name = calledExpr.declName.baseName.text + + if name == "onTapGesture" { + if arguments.isEmpty { return true } // Default count is 1 + if let countArg = arguments.first(where: { $0.label?.text == "count" }) { + return countArg.expression.as(IntegerLiteralExprSyntax.self)?.literal.text == "1" + } + // If 'count' is not specified, it defaults to 1 (other args like 'perform' might be present) + return !arguments.contains(where: { $0.label?.text == "count" }) + } + + if ["gesture", "simultaneousGesture", "highPriorityGesture"].contains(name) { + guard let firstArgExpression = arguments.first?.expression else { return false } + return AccessibilityButtonTraitDeterminator.isSingleTapGestureInstance(expression: firstArgExpression) + } + return false } - /// Whether or not the dictionary represents a SwiftUI View with an `accessibilityAddTraits()` or - /// `accessibility(addTraits:)` modifier with the specified trait (specify trait as a String). - func hasAccessibilityTrait(_ trait: String, in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityAddTraits", - arguments: [.init(name: "", values: [trait], matchType: .substring)] - ), - SwiftUIModifier( - name: "accessibility", - arguments: [.init(name: "addTraits", values: [trait], matchType: .substring)] - ), - ], - in: file - ) + func providesButtonOrLinkTrait() -> Bool { + guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false } + let name = calledExpr.declName.baseName.text + + if name == "accessibilityAddTraits" { + guard let firstArgExpr = arguments.first?.expression else { return false } + return Self.expressionContainsButtonOrLinkTrait(firstArgExpr) + } + + if name == "accessibility" { + guard let addTraitsArg = arguments.first(where: { $0.label?.text == "addTraits" }) else { return false } + return Self.expressionContainsButtonOrLinkTrait(addTraitsArg.expression) + } + + return false + } + + private static func expressionContainsButtonOrLinkTrait(_ expression: ExprSyntax) -> Bool { + if let memberAccess = expression.as(MemberAccessExprSyntax.self) { + let traitName = memberAccess.declName.baseName.text + return traitName == "isButton" || traitName == "isLink" + } + if let arrayExpr = expression.as(ArrayExprSyntax.self) { + return arrayExpr.elements.contains { element in + self.expressionContainsButtonOrLinkTrait(element.expression) + } + } + return false + } + + func isAccessibilityHiddenTrue() -> Bool { + guard let calledExpr = calledExpression.as(MemberAccessExprSyntax.self) else { return false } + let name = calledExpr.declName.baseName.text + + if name == "accessibilityHidden" { + return arguments.first?.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + } + + if name == "accessibility" { + guard let hiddenArg = arguments.first(where: { $0.label?.text == "hidden" }) else { return false } + return hiddenArg.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + } + + return false + } + + func isModifierChainRoot() -> Bool { + // Check if this function call is at the root of a modifier chain + // (i.e., it's the topmost expression in a chain like Text().modifier1().modifier2()) + guard let memberAccess = calledExpression.as(MemberAccessExprSyntax.self) else { + return false // Direct function calls like Text() are not modifier chain roots + } + return memberAccess.base != nil // Has a base, so it's part of a modifier chain } } From cc03c83eb922c9e564f70072a38c2dfe6d3791ca Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 11:01:20 -0400 Subject: [PATCH 30/80] Migrate ClosureEndIndentationRule from SourceKit to SwiftSyntax (#6109) * Migrate ClosureEndIndentationRule from SourceKit to SwiftSyntax - Converted rule to SwiftSyntax - Implemented recursive anchor detection for method chains - Fixed indentation calculation for trailing closures - Fixed correction logic to properly handle existing whitespace * Revert debug changes to OSSCheck * Count whitespace for indentation, not first non-whitespace position * Simplify `hasNewlineInTrivia` calculation * Update changelog --- CHANGELOG.md | 18 +- .../Style/ClosureEndIndentationRule.swift | 459 ++++++------------ .../ClosureEndIndentationRuleExamples.swift | 8 +- 3 files changed, 163 insertions(+), 322 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc58386732..959a24dc67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,21 +22,15 @@ [imsonalbajaj](https://github.com/imsonalbajaj) [#6054](https://github.com/realm/SwiftLint/issues/6054) -* Migrate `file_length` rule from SourceKit to SwiftSyntax for improved performance - and fewer false positives. +* Rewrite the following rules with SwiftSyntax: + * `accessibility_label_for_image` + * `accessibility_trait_for_button` + * `closure_end_indentation` + * `file_length` + * `vertical_whitespace` [JP Simard](https://github.com/jpsim) - -* Migrate `vertical_whitespace` rule from SourceKit to SwiftSyntax for improved performance. [Matt Pennig](https://github.com/pennig) -* Migrate `accessibility_label_for_image` rule from SourceKit to SwiftSyntax for improved - performance and fewer false positives. - [JP Simard](https://github.com/jpsim) - -* Migrate `accessibility_trait_for_button` rule from SourceKit to SwiftSyntax for improved - performance and fewer false positives. - [JP Simard](https://github.com/jpsim) - ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift index 9782d141af..0633aaee38 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift @@ -1,7 +1,8 @@ -import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax -struct ClosureEndIndentationRule: Rule, OptInRule { +@SwiftSyntaxRule(correctable: true, optIn: true) +struct ClosureEndIndentationRule: Rule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -13,334 +14,180 @@ struct ClosureEndIndentationRule: Rule, OptInRule { triggeringExamples: ClosureEndIndentationRuleExamples.triggeringExamples, corrections: ClosureEndIndentationRuleExamples.corrections ) - - fileprivate static let notWhitespace = regex("[^\\s]") - - func validate(file: SwiftLintFile) -> [StyleViolation] { - violations(in: file).map { violation in - styleViolation(for: violation, in: file) - } - } - - private func styleViolation(for violation: Violation, in file: SwiftLintFile) -> StyleViolation { - let reason = "Closure end should have the same indentation as the line that started it; " + - "expected \(violation.indentationRanges.expected.length), " + - "got \(violation.indentationRanges.actual.length)" - - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: violation.endOffset), - reason: reason) - } } -extension ClosureEndIndentationRule: CorrectableRule { - func correct(file: SwiftLintFile) -> Int { - let allViolations = violations(in: file).reversed().filter { violation in - guard let nsRange = file.stringView.byteRangeToNSRange(violation.range) else { - return false +private extension ClosureEndIndentationRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ClosureExprSyntax) { + // Get locations of opening and closing braces + let leftBraceLocation = locationConverter.location( + for: node.leftBrace.positionAfterSkippingLeadingTrivia + ) + let rightBracePositionAfterTrivia = node.rightBrace.positionAfterSkippingLeadingTrivia + let rightBraceLocation = locationConverter.location(for: rightBracePositionAfterTrivia) + + // Only interested in multi-line closures + let leftBraceLine = leftBraceLocation.line + let rightBraceLine = rightBraceLocation.line + guard rightBraceLine > leftBraceLine else { + return } - return file.ruleEnabled(violatingRanges: [nsRange], for: self).isNotEmpty - } - - guard allViolations.isNotEmpty else { - return 0 - } - - var correctedContents = file.contents - var correctedLocations: [Int] = [] - - let actualLookup = actualViolationLookup(for: allViolations) - - for violation in allViolations { - let expected = actualLookup(violation).indentationRanges.expected - let actual = violation.indentationRanges.actual - if correct(contents: &correctedContents, expected: expected, actual: actual) { - correctedLocations.append(actual.location) + // Find the position that the closing brace should align with + guard let anchorPosition = findAnchorPosition(for: node) else { + return } - } - - var numberOfCorrections = correctedLocations.count - file.write(correctedContents) - - // Re-correct to catch cascading indentation from the first round. - numberOfCorrections += correct(file: file) - - return numberOfCorrections - } - - private func correct(contents: inout String, expected: NSRange, actual: NSRange) -> Bool { - guard let actualIndices = contents.nsrangeToIndexRange(actual) else { - return false - } - - let regex = Self.notWhitespace - if regex.firstMatch(in: contents, options: [], range: actual) != nil { - var correction = "\n" - correction.append(contents.substring(from: expected.location, length: expected.length)) - contents.insert(contentsOf: correction, at: actualIndices.upperBound) - } else { - let correction = contents.substring(from: expected.location, length: expected.length) - contents = contents.replacingCharacters(in: actualIndices, with: correction) - } - - return true - } - - private func actualViolationLookup(for violations: [Violation]) -> (Violation) -> Violation { - let lookup = violations.reduce(into: [NSRange: Violation](), { result, violation in - result[violation.indentationRanges.actual] = violation - }) - - func actualViolation(for violation: Violation) -> Violation { - guard let actual = lookup[violation.indentationRanges.expected] else { return violation } - return actualViolation(for: actual) - } - - return actualViolation - } -} - -extension ClosureEndIndentationRule { - fileprivate struct Violation { - var indentationRanges: (expected: NSRange, actual: NSRange) - var endOffset: ByteCount - var range: ByteRange - } - - fileprivate func violations(in file: SwiftLintFile) -> [Violation] { - file.structureDictionary.traverseDepthFirst { subDict in - guard let kind = subDict.expressionKind else { return nil } - return violations(in: file, of: kind, dictionary: subDict) - } - } - - private func violations(in file: SwiftLintFile, - of kind: SwiftExpressionKind, - dictionary: SourceKittenDictionary) -> [Violation] { - guard kind == .call else { - return [] - } - - var violations = validateArguments(in: file, dictionary: dictionary) - - if let callViolation = validateCall(in: file, dictionary: dictionary) { - violations.append(callViolation) - } - - return violations - } - - private func hasTrailingClosure(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> Bool { - guard - let byteRange = dictionary.byteRange, - let text = file.stringView.substringWithByteRange(byteRange) - else { - return false - } - return !text.hasSuffix(")") - } - - private func validateCall(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> Violation? { - let contents = file.stringView - guard let offset = dictionary.offset, - let length = dictionary.length, - let bodyLength = dictionary.bodyLength, - let nameOffset = dictionary.nameOffset, - let nameLength = dictionary.nameLength, - bodyLength > 0, - case let endOffset = offset + length - 1, - case let closingBraceByteRange = ByteRange(location: endOffset, length: 1), - contents.substringWithByteRange(closingBraceByteRange) == "}", - let startOffset = startOffset(forDictionary: dictionary, file: file), - let (startLine, _) = contents.lineAndCharacter(forByteOffset: startOffset), - let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), - case let nameEndPosition = nameOffset + nameLength, - let (bodyOffsetLine, _) = contents.lineAndCharacter(forByteOffset: nameEndPosition), - startLine != endLine, bodyOffsetLine != endLine, - !containsSingleLineClosure(dictionary: dictionary, endPosition: endOffset, file: file) - else { - return nil - } - - let range = file.lines[startLine - 1].range - let regex = Self.notWhitespace - let actual = endPosition - 1 - guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range, - case let expected = match.location - range.location, - expected != actual - else { - return nil - } - - var expectedRange = range - expectedRange.length = expected - - var actualRange = file.lines[endLine - 1].range - actualRange.length = actual - - return Violation(indentationRanges: (expected: expectedRange, actual: actualRange), - endOffset: endOffset, - range: ByteRange(location: offset, length: length)) - } - - private func validateArguments(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> [Violation] { - guard isFirstArgumentOnNewline(dictionary, file: file) else { - return [] - } - - var closureArguments = filterClosureArguments(dictionary.enclosedArguments, file: file) - - if hasTrailingClosure(in: file, dictionary: dictionary), closureArguments.isNotEmpty { - closureArguments.removeLast() - } - - return closureArguments.compactMap { dictionary in - validateClosureArgument(in: file, dictionary: dictionary) - } - } - - private func validateClosureArgument(in file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> Violation? { - let contents = file.stringView - guard let offset = dictionary.offset, - let length = dictionary.length, - let bodyLength = dictionary.bodyLength, - let nameOffset = dictionary.nameOffset, - let nameLength = dictionary.nameLength, - bodyLength > 0, - case let endOffset = offset + length - 1, - case let closingBraceByteRange = ByteRange(location: endOffset, length: 1), - contents.substringWithByteRange(closingBraceByteRange) == "}", - let startOffset = dictionary.offset, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: startOffset), - let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), - case let nameEndPosition = nameOffset + nameLength, - let (bodyOffsetLine, _) = contents.lineAndCharacter(forByteOffset: nameEndPosition), - startLine != endLine, bodyOffsetLine != endLine, - !isSingleLineClosure(dictionary: dictionary, endPosition: endOffset, file: file) - else { - return nil + let anchorLocation = locationConverter.location(for: anchorPosition) + let anchorLineNumber = anchorLocation.line + + // Calculate expected indentation + let expectedIndentationColumn = getFirstNonWhitespaceColumn(onLine: anchorLineNumber) - 1 + + // Calculate actual indentation of the closing brace + let actualIndentationColumn = rightBraceLocation.column - 1 + + if actualIndentationColumn != expectedIndentationColumn { + let correctionStart: AbsolutePosition + let correctionReplacement: String + + // Check if there's leading trivia on the right brace that contains a newline + let hasNewlineInTrivia = node.rightBrace.leadingTrivia.contains(where: \.isNewline) + + if hasNewlineInTrivia { + // If there's already a newline, we just need to fix the indentation. + // The range to replace is the trivia before the brace. + correctionStart = node.rightBrace.position + correctionReplacement = "\n" + String(repeating: " ", count: max(0, expectedIndentationColumn)) + } else if let previousToken = node.rightBrace.previousToken(viewMode: .sourceAccurate) { + // If no newline, we need to add one. The replacement will be inserted + // after the previous token and before the closing brace. + correctionStart = previousToken.endPositionBeforeTrailingTrivia + correctionReplacement = "\n" + String(repeating: " ", count: max(0, expectedIndentationColumn)) + } else { + // Fallback: If there's no previous token, which is unlikely for a closure body, + // replace the trivia before the brace. + correctionStart = node.rightBrace.position + correctionReplacement = "\n" + String(repeating: " ", count: max(0, expectedIndentationColumn)) + } + + let reason = "expected \(expectedIndentationColumn), got \(actualIndentationColumn)" + violations.append( + ReasonedRuleViolation( + position: node.rightBrace.positionAfterSkippingLeadingTrivia, + reason: reason, + severity: configuration.severity, + correction: .init( + start: correctionStart, + end: node.rightBrace.positionAfterSkippingLeadingTrivia, + replacement: correctionReplacement + ) + ) + ) + } } - let range = file.lines[startLine - 1].range - let regex = Self.notWhitespace - let actual = endPosition - 1 - guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range, - case let expected = match.location - range.location, - expected != actual - else { - return nil - } + /// Finds the position of a token that the closure's closing brace should be aligned with. + private func findAnchorPosition(for closureNode: ClosureExprSyntax) -> AbsolutePosition? { + guard let parent = closureNode.parent else { + return nil + } - var expectedRange = range - expectedRange.length = expected + // Case: Trailing closure. e.g., `list.map { ... }` + if let functionCall = parent.as(FunctionCallExprSyntax.self), + closureNode.id == functionCall.trailingClosure?.id { + return anchor(for: ExprSyntax(functionCall)) + } - var actualRange = file.lines[endLine - 1].range - actualRange.length = actual + // Case: Closure as a labeled argument. e.g., `function(label: { ... })` + if let labeledExpr = parent.as(LabeledExprSyntax.self) { + // Check if this is part of a function call where the first argument is on a new line + if let argList = labeledExpr.parent?.as(LabeledExprListSyntax.self), + let functionCall = argList.parent?.as(FunctionCallExprSyntax.self), + let firstArg = argList.first, + let leftParen = functionCall.leftParen { + // Get the location of the opening paren and first argument + let leftParenLocation = locationConverter.location( + for: leftParen.positionAfterSkippingLeadingTrivia + ) + let firstArgLocation = locationConverter.location( + for: firstArg.positionAfterSkippingLeadingTrivia + ) + + // If first argument is on the same line as the opening paren, don't apply the rule + if leftParenLocation.line == firstArgLocation.line { + return nil + } + } + + // The anchor is the start of the argument expression (including the label). + if let label = labeledExpr.label { + return label.positionAfterSkippingLeadingTrivia + } + return labeledExpr.positionAfterSkippingLeadingTrivia + } - return Violation(indentationRanges: (expected: expectedRange, actual: actualRange), - endOffset: endOffset, - range: ByteRange(location: offset, length: length)) - } + // Case: Multiple trailing closures. e.g., `function { ... } another: { ... }` + if let multipleTrailingClosure = parent.as(MultipleTrailingClosureElementSyntax.self) { + // The anchor is the label of the specific trailing closure. + return multipleTrailingClosure.label.positionAfterSkippingLeadingTrivia + } - private func startOffset(forDictionary dictionary: SourceKittenDictionary, file: SwiftLintFile) -> ByteCount? { - guard let nameByteRange = dictionary.nameByteRange else { - return nil - } + // For closures on new lines after function calls + if let exprList = parent.as(ExprListSyntax.self), + exprList.count == 1, + exprList.parent?.as(FunctionCallExprSyntax.self) != nil { + // This is a closure on its own line after a function call like: + // foo(abc, 123) + // { _ in } + return closureNode.positionAfterSkippingLeadingTrivia + } - let newLineRegex = regex("\n(\\s*\\}?\\.)") - let contents = file.stringView - guard let range = contents.byteRangeToNSRange(nameByteRange), - let match = newLineRegex.matches(in: file.contents, options: [], range: range).last?.range(at: 1), - let methodByteRange = contents.NSRangeToByteRange(start: match.location, length: match.length) - else { - return nameByteRange.location + // Fallback for other cases (e.g., closure in an array literal). + // The anchor is the start of the parent syntax node. + return closureNode.positionAfterSkippingLeadingTrivia } - return methodByteRange.location - } + /// Recursively traverses a chain of expressions (e.g., member access or function calls) + /// to find the token that begins the statement. This is the token that the closure's + /// closing brace should ultimately be aligned with. + /// - Parameter expression: The expression to find the anchor for. + /// - Returns: The absolute position of the anchor token. + private func anchor(for expression: ExprSyntax) -> AbsolutePosition { + if let memberAccess = expression.as(MemberAccessExprSyntax.self), let base = memberAccess.base { + let baseAnchor = anchor(for: base) - private func isSingleLineClosure(dictionary: SourceKittenDictionary, - endPosition: ByteCount, - file: SwiftLintFile) -> Bool { - let contents = file.stringView + let memberStartPosition = memberAccess.period.positionAfterSkippingLeadingTrivia + let baseEndPosition = base.endPositionBeforeTrailingTrivia - guard let start = dictionary.bodyOffset, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: start), - let (endLine, _) = contents.lineAndCharacter(forByteOffset: endPosition) else { - return false - } - - return startLine == endLine - } + let memberStartLocation = locationConverter.location(for: memberStartPosition) + let baseEndLocation = locationConverter.location(for: baseEndPosition) - private func containsSingleLineClosure(dictionary: SourceKittenDictionary, - endPosition: ByteCount, - file: SwiftLintFile) -> Bool { - let contents = file.stringView - - guard let closure = trailingClosure(dictionary: dictionary, file: file), - let start = closure.bodyOffset, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: start), - let (endLine, _) = contents.lineAndCharacter(forByteOffset: endPosition) else { - return false - } + if memberStartLocation.line > baseEndLocation.line { + return memberStartPosition + } - return startLine == endLine - } + return baseAnchor + } - private func trailingClosure(dictionary: SourceKittenDictionary, - file: SwiftLintFile) -> SourceKittenDictionary? { - let arguments = dictionary.enclosedArguments - let closureArguments = filterClosureArguments(arguments, file: file) + if let functionCall = expression.as(FunctionCallExprSyntax.self) { + return anchor(for: functionCall.calledExpression) + } - if closureArguments.count == 1, - closureArguments.last?.offset == arguments.last?.offset { - return closureArguments.last + return expression.positionAfterSkippingLeadingTrivia } - return nil - } - - private func filterClosureArguments(_ arguments: [SourceKittenDictionary], - file: SwiftLintFile) -> [SourceKittenDictionary] { - arguments.filter { argument in - guard let bodyByteRange = argument.bodyByteRange, - let range = file.stringView.byteRangeToNSRange(bodyByteRange), - let match = regex("\\s*\\{").firstMatch(in: file.contents, options: [], range: range)?.range, - match.location == range.location - else { - return false + /// Calculates the column of the first non-whitespace character on a given line. + private func getFirstNonWhitespaceColumn(onLine lineNumber: Int) -> Int { + guard lineNumber > 0, lineNumber <= file.lines.count else { + return 1 // Should not happen } + let lineContent = file.lines[lineNumber - 1].content - return true - } - } - - private func isFirstArgumentOnNewline(_ dictionary: SourceKittenDictionary, - file: SwiftLintFile) -> Bool { - guard - let nameOffset = dictionary.nameOffset, - let nameLength = dictionary.nameLength, - let firstArgument = dictionary.enclosedArguments.first, - let firstArgumentOffset = firstArgument.offset, - case let offset = nameOffset + nameLength, - case let length = firstArgumentOffset - offset, - length > 0, - case let byteRange = ByteRange(location: offset, length: length), - let range = file.stringView.byteRangeToNSRange(byteRange), - let match = regex("\\(\\s*\\n\\s*").firstMatch(in: file.contents, options: [], range: range)?.range, - match.location == range.location - else { - return false + if let firstCharIndex = lineContent.firstIndex(where: { !$0.isWhitespace }) { + return lineContent.distance(from: lineContent.startIndex, to: firstCharIndex) + 1 + } + return 1 // Empty or whitespace-only line } - - return true } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift index e49a4a7aca..29ade3afdf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift @@ -64,7 +64,7 @@ internal struct ClosureEndIndentationRuleExamples { return Command(string: contents, range: range) ↓}.flatMap { command in return command.expand() - ↓} + } """), Example(""" function( @@ -139,7 +139,7 @@ internal struct ClosureEndIndentationRuleExamples { """): Example(""" function( closure: { x in - print(x) \("") + print(x) }) """), Example(""" @@ -214,7 +214,7 @@ internal struct ClosureEndIndentationRuleExamples { """): Example(""" function( closure: { x in - print(x) \("") + print(x) }, anotherClosure: { y in print(y) @@ -233,7 +233,7 @@ internal struct ClosureEndIndentationRuleExamples { print(x) }, anotherClosure: { y in print(y) - }) + }) """), ] } From 18403e86049e7101269c2ad57d329ee7eb52070c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 21 Jun 2025 17:15:25 +0200 Subject: [PATCH 31/80] Harmonize violation positions (#6124) --- .../Rules/Metrics/FunctionBodyLengthRule.swift | 2 +- Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index a2de07b288..23c4949465 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -19,7 +19,7 @@ private extension FunctionBodyLengthRule { registerViolations( leftBrace: body.leftBrace, rightBrace: body.rightBrace, - violationNode: node.name, + violationNode: node.funcKeyword, objectName: "Function" ) } diff --git a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift index f8acfbaa2e..ba6601d8f1 100644 --- a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift +++ b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift @@ -25,7 +25,7 @@ final class FunctionBodyLengthRuleTests: SwiftLintTestCase { [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 6), + location: Location(file: nil, line: 1, character: 1), reason: "Function body should span 50 lines or less excluding comments and " + "whitespace: currently spans 51 lines" ), @@ -54,7 +54,7 @@ final class FunctionBodyLengthRuleTests: SwiftLintTestCase { [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 6), + location: Location(file: nil, line: 1, character: 1), reason: "Function body should span 50 lines or less excluding comments and " + "whitespace: currently spans 51 lines" ), @@ -78,7 +78,7 @@ final class FunctionBodyLengthRuleTests: SwiftLintTestCase { [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 6), + location: Location(file: nil, line: 1, character: 1), reason: "Function body should span 50 lines or less excluding comments and " + "whitespace: currently spans 51 lines" ), From 286e67ff2312970d5131b6b3b545900f59358050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 21 Jun 2025 17:17:55 +0200 Subject: [PATCH 32/80] Report more specific target types (#6123) --- .../Rules/Metrics/FunctionBodyLengthRule.swift | 2 +- .../Rules/Metrics/TypeBodyLengthRule.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index 23c4949465..e5df6e56df 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -31,7 +31,7 @@ private extension FunctionBodyLengthRule { leftBrace: body.leftBrace, rightBrace: body.rightBrace, violationNode: node.initKeyword, - objectName: "Function" + objectName: "Initializer" ) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index 622ce03d65..c9978a5343 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -58,7 +58,7 @@ private extension TypeBodyLengthRule { leftBrace: node.memberBlock.leftBrace, rightBrace: node.memberBlock.rightBrace, violationNode: node.introducer, - objectName: "Type" + objectName: node.introducer.text.capitalized ) } } From 45c4766a020f61d89fdb12b3db881fdf03ea4cfb Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 11:34:06 -0400 Subject: [PATCH 33/80] Migrate AccessibilityLabelForImageRule from SourceKit to SwiftSyntax (#6101) * Migrate AccessibilityLabelForImageRule from SourceKit to SwiftSyntax \## Summary Fix `.accessibilityElement(children:)` exemption logic where the previous implementation incorrectly exempted images with `.combine` when only `.ignore` should exempt children. \## Improvements Based on OSSCheck Results \### New Violations (Legitimately Detected) - `Image(uiImage:)` and `Image(nsImage:)` calls providing contextual content - Previously missed by SourceKit's pattern matching limitations - These represent real accessibility issues requiring labels \### Fixed Violations (False Positives Removed) - Images inside Button/NavigationLink labels (containers handle accessibility) - Images in custom components with proper accessibility context - System icons incorrectly flagged when already accessible \## Key Technical Improvements - **Comprehensive detection** of all Image initializer types - **Better context awareness** through syntax tree traversal - **Accurate `.accessibilityElement(children:)` behavior** (`.ignore` exempts, `.combine`/`.contain` do not) - **Reduced false positives** while catching real accessibility issues \## Regression Test Examples Added comprehensive examples demonstrating the migration benefits: \### Non-Triggering (Reduced False Positives) - Images in `Button`/`NavigationLink` labels with accessibility context - Images in containers with `.accessibilityElement(children: .ignore)` - Complex hierarchies where container accessibility handles children - Various Image initializer types in proper accessible contexts \### Triggering (Improved Detection) - `Image(uiImage:)` and `Image(nsImage:)` providing contextual content - Images in containers with `.combine`/`.contain` that don't exempt - Status icons, favicons, and background images needing labels * Update changelog --- .../Lint/AccessibilityLabelForImageRule.swift | 311 +++++++++++++----- ...cessibilityLabelForImageRuleExamples.swift | 170 ++++++++++ 2 files changed, 395 insertions(+), 86 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift index 57cf76b26e..9f816b007f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift @@ -1,6 +1,7 @@ -import SourceKittenFramework +import SwiftSyntax -struct AccessibilityLabelForImageRule: ASTRule, OptInRule { +@SwiftSyntaxRule +struct AccessibilityLabelForImageRule: Rule, OptInRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( @@ -24,121 +25,259 @@ struct AccessibilityLabelForImageRule: ASTRule, OptInRule { nonTriggeringExamples: AccessibilityLabelForImageRuleExamples.nonTriggeringExamples, triggeringExamples: AccessibilityLabelForImageRuleExamples.triggeringExamples ) +} - // MARK: AST Rule +private extension AccessibilityLabelForImageRule { + final class Visitor: ViolationsSyntaxVisitor { + private var isInViewStruct = false - func validate(file: SwiftLintFile, - kind: SwiftDeclarationKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - // Only proceed to check View structs. - guard kind == .struct, - dictionary.inheritedTypes.contains("View"), - dictionary.substructure.isNotEmpty else { - return [] + override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { + isInViewStruct = node.isViewStruct + return .visitChildren } - return findImageViolations(file: file, substructure: dictionary.substructure) - } + override func visitPost(_: StructDeclSyntax) { + isInViewStruct = false + } - /// Recursively check a file for image violations, and return all such violations. - private func findImageViolations(file: SwiftLintFile, substructure: [SourceKittenDictionary]) -> [StyleViolation] { - var violations = [StyleViolation]() - for dictionary in substructure { - guard let offset: ByteCount = dictionary.offset else { - continue - } + override func visitPost(_ node: FunctionCallExprSyntax) { + // Only check Image calls within View structs + guard isInViewStruct else { return } - // If it's image, and does not hide from accessibility or provide a label, it's a violation. - if dictionary.isImage { - if dictionary.isDecorativeOrLabeledImage || - dictionary.hasAccessibilityHiddenModifier(in: file) || - dictionary.hasAccessibilityLabelModifier(in: file) { - continue - } + // Only process direct Image calls + guard node.isDirectImageCall else { return } - violations.append( - StyleViolation(ruleDescription: Self.description, - severity: configuration.severity, - location: Location(file: file, byteOffset: offset)) + // Use centralized exemption logic + if !AccessibilityDeterminator.isExempt(node) { + let violation = ReasonedRuleViolation( + position: node.positionAfterSkippingLeadingTrivia, + reason: """ + Images that provide context should have an accessibility label or should be \ + explicitly hidden from accessibility + """, + severity: configuration.severity ) + violations.append(violation) } + } + } +} - // If dictionary did not represent an Image, recursively check substructure, - // unless it's a container that hides its children from accessibility or is labeled. - else if dictionary.substructure.isNotEmpty { - if dictionary.hasAccessibilityHiddenModifier(in: file) || - dictionary.hasAccessibilityElementChildrenIgnoreModifier(in: file) || - dictionary.hasAccessibilityLabelModifier(in: file) { - continue - } +// MARK: Accessibility Exemption Logic - violations.append(contentsOf: findImageViolations(file: file, substructure: dictionary.substructure)) - } +private struct AccessibilityDeterminator { + /// Maximum depth to search up the syntax tree for exemptions + static let maxSearchDepth = 20 + + /// Determines if an Image call is exempt from requiring accessibility treatment + static func isExempt(_ imageCall: FunctionCallExprSyntax) -> Bool { + // 1. Check for decorative or labeled initializers (e.g., Image(decorative:)) + if imageCall.isDecorativeOrLabeledImage { + return true } - return violations + // 2. Check the parent hierarchy for exemptions + return imageCall.isExemptedByAncestors() } } -// MARK: SourceKittenDictionary extensions +// MARK: SwiftSyntax extensions -private extension SourceKittenDictionary { - /// Whether or not the dictionary represents a SwiftUI Image. - /// Currently only accounts for SwiftUI image literals and not instance variables. - var isImage: Bool { - // Image literals will be reported as calls to the initializer. - guard expressionKind == .call else { - return false +private extension StructDeclSyntax { + /// Whether this struct conforms to View protocol + var isViewStruct: Bool { + guard let inheritanceClause else { return false } + + return inheritanceClause.inheritedTypes.contains { inheritedType in + inheritedType.type.as(IdentifierTypeSyntax.self)?.name.text == "View" } + } +} - if name == "Image" || name == "SwiftUI.Image" { - return true +private extension FunctionCallExprSyntax { + /// Check if this is a direct Image call (not a modifier) + var isDirectImageCall: Bool { + // Check for direct Image call + if let identifierExpr = calledExpression.as(DeclReferenceExprSyntax.self) { + return identifierExpr.baseName.text == "Image" + } + + // Check for SwiftUI.Image call + if let memberAccessExpr = calledExpression.as(MemberAccessExprSyntax.self), + let baseIdentifier = memberAccessExpr.base?.as(DeclReferenceExprSyntax.self) { + return baseIdentifier.baseName.text == "SwiftUI" && + memberAccessExpr.declName.baseName.text == "Image" } - // Recursively check substructure. - // SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like: - // Image(decorative: "myImage").resizable().frame - // --> Image(decorative: "myImage").resizable - // --> Image - return substructure.contains(where: \.isImage) + return false } - /// Whether or not the dictionary represents a SwiftUI Image using the `Image(decorative:)` constructor (hides - /// from a11y), or the `Image(_:label:)` constructors (which provide labels). + /// Whether this is Image(decorative:) or Image(_:label:) var isDecorativeOrLabeledImage: Bool { - guard isImage else { + arguments.contains { arg in + let label = arg.label?.text + return label == "decorative" || label == "label" + } + } + + /// Walks up the syntax tree to find accessibility exemptions with depth limit + func isExemptedByAncestors() -> Bool { + var currentNode: Syntax? = Syntax(self) + var depth = 0 + + while let node = currentNode, depth < AccessibilityDeterminator.maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + // Check function calls for exempting patterns + guard let funcCall = node.as(FunctionCallExprSyntax.self) else { continue } + + // Check for accessibility modifiers + if let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) { + let modifierName = memberAccess.declName.baseName.text + + if funcCall.isDirectAccessibilityModifier(modifierName) || + funcCall.isContainerExemptingModifier(modifierName) { + return true + } + } + + // Check for inherently exempting containers + if funcCall.isInherentlyExemptingContainer() { + return true + } + + // Check container views with accessibility modifiers + if funcCall.isContainerView() && funcCall.hasAccessibilityModifiersInChain() { + return true + } + + // Stop early at statement boundaries for performance + if node.parent?.is(StmtSyntax.self) == true { + break + } + } + + return false + } + + /// Check if this function call represents a container view + func isContainerView() -> Bool { + guard let identifierExpr = calledExpression.as(DeclReferenceExprSyntax.self) else { return false } + let containerNames: Set = ["VStack", "HStack", "ZStack", "Group", "LazyVStack", "LazyHStack"] + return containerNames.contains(identifierExpr.baseName.text) + } + + /// Check if this container has accessibility modifiers in its modifier chain + func hasAccessibilityModifiersInChain() -> Bool { + var currentNode: Syntax? = Syntax(self) + var depth = 0 + + while let node = currentNode, depth < AccessibilityDeterminator.maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + guard let funcCall = node.as(FunctionCallExprSyntax.self), + let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) else { continue } + + let modifierName = memberAccess.declName.baseName.text + + if funcCall.isDirectAccessibilityModifier(modifierName) || + funcCall.isContainerExemptingModifier(modifierName) { + return true + } + + // Stop at statement boundaries + if node.parent?.is(StmtSyntax.self) == true { + break + } + } + + return false + } + + /// Checks for modifiers like .accessibilityLabel(...) or .accessibilityHidden(true) + func isDirectAccessibilityModifier(_ name: String) -> Bool { + switch name { + case "accessibilityHidden": + return arguments.first?.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + case "accessibilityLabel", "accessibilityValue", "accessibilityHint": + return true + case "accessibility": + return arguments.contains { arg in + guard let label = arg.label?.text else { return false } + if ["label", "value", "hint"].contains(label) { return true } + if label == "hidden" { + return arg.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind == .keyword(.true) + } + return false + } + default: return false } + } + + /// Checks for modifiers that make a container exempt its children from individual accessibility + func isContainerExemptingModifier(_ name: String) -> Bool { + guard name == "accessibilityElement" else { return false } - // Check for Image(decorative:) or Image(_:label:) constructor. - if expressionKind == .call && - enclosedArguments.contains(where: { ["decorative", "label"].contains($0.name) }) { + // Check for .accessibilityElement(children: .ignore) which exempts children + if let childrenArg = arguments.first(where: { $0.label?.text == "children" }) { + let childrenValue = childrenArg.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text + return childrenValue == "ignore" // Only .ignore exempts individual children + } + + // .accessibilityElement() with no arguments defaults to behavior that exempts children + return arguments.isEmpty + } + + /// Checks for container views that provide their own accessibility context + func isInherentlyExemptingContainer() -> Bool { + guard let identifier = calledExpression.as(DeclReferenceExprSyntax.self) else { return false } + let containerName = identifier.baseName.text + + // NavigationLink automatically exempts children + if containerName == "NavigationLink" { return true } - // Recursively check substructure. - // SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like: - // Image(decorative: "myImage").resizable().frame - // --> Image(decorative: "myImage").resizable - // --> Image - return substructure.contains(where: \.isDecorativeOrLabeledImage) + // Button exempts children if it has accessibility treatment + if containerName == "Button" { + return hasDirectAccessibilityTreatment() + } + + return false } - /// Whether or not the dictionary represents a SwiftUI View with an `accesibilityLabel(_:)` - /// or `accessibility(label:)` modifier. - func hasAccessibilityLabelModifier(in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityLabel", - arguments: [.init(name: "", values: [])] - ), - SwiftUIModifier( - name: "accessibility", - arguments: [.init(name: "label", values: [])] - ), - ], - in: file - ) + /// Check if this container has direct accessibility treatment + private func hasDirectAccessibilityTreatment() -> Bool { + var currentNode: Syntax? = Syntax(self) + var depth = 0 + + while let node = currentNode, depth < AccessibilityDeterminator.maxSearchDepth { + defer { + currentNode = node.parent + depth += 1 + } + + guard let funcCall = node.as(FunctionCallExprSyntax.self), + let memberAccess = funcCall.calledExpression.as(MemberAccessExprSyntax.self) else { continue } + + let modifierName = memberAccess.declName.baseName.text + if funcCall.isDirectAccessibilityModifier(modifierName) { + return true + } + + // Stop at statement boundaries + if node.parent?.is(StmtSyntax.self) == true { + break + } + } + + return false } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift index ede1e90e84..8901f882e7 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift @@ -1,3 +1,6 @@ +// swiftlint:disable file_length + +// swiftlint:disable:next type_body_length internal struct AccessibilityLabelForImageRuleExamples { static let nonTriggeringExamples = [ Example(""" @@ -149,6 +152,112 @@ internal struct AccessibilityLabelForImageRuleExamples { } } """), + // MARK: - SwiftSyntax Migration Regression Tests + // These examples would have been false positives with the SourceKit implementation + // but now correctly pass with the SwiftSyntax implementation + Example(""" + struct MyView: View { + var body: some View { + NavigationLink("Go to Details") { + DetailView() + } label: { + HStack { + Image(systemName: "arrow.right") + Text("Navigate Here") + } + } + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + Button("Save Changes") { + saveAction() + } label: { + Label("Save", systemImage: "square.and.arrow.down") + } + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + Button(action: performAction) { + HStack { + Image(uiImage: UIImage(systemName: "star") ?? UIImage()) + Text("Favorite") + } + } + .accessibilityLabel("Add to Favorites") + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + VStack { + Image(systemName: "wifi") + Image("network-icon") + Text("Network Status") + }.accessibilityElement(children: .ignore) + .accessibilityLabel("Connected to WiFi") + } + } + """), + Example(""" + struct MyView: View { + let statusImage: UIImage + var body: some View { + HStack { + Image(uiImage: statusImage) + .foregroundColor(.green) + Text("System Status") + }.accessibilityElement(children: .ignore) + .accessibilityLabel("System is operational") + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + NavigationLink(destination: SettingsView()) { + HStack { + Image(nsImage: NSImage(named: "gear") ?? NSImage()) + Text("Preferences") + Spacer() + Image(systemName: "chevron.right") + } + } + } + } + """), + Example(""" + struct MyView: View { + var body: some View { + Button { + toggleState() + } label: { + Image(systemName: isEnabled ? "eye" : "eye.slash") + .foregroundColor(isEnabled ? .blue : .gray) + } + .accessibilityLabel(isEnabled ? "Hide content" : "Show content") + } + } + """), + Example(""" + struct CustomCard: View { + var body: some View { + VStack { + Image("card-background") + Image(systemName: "checkmark.circle") + Text("Task Complete") + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Task completed successfully") + } + } + """), ] static let triggeringExamples = [ @@ -265,5 +374,66 @@ internal struct AccessibilityLabelForImageRuleExamples { } } """), + // MARK: - SwiftSyntax Migration Detection Improvements + // These violations would have been missed by the SourceKit implementation + // but are now correctly detected by SwiftSyntax + Example(""" + struct StatusView: View { + let statusIcon: UIImage + var body: some View { + HStack { + ↓Image(uiImage: statusIcon) + .foregroundColor(.green) + Text("Status") + } + } + } + """), + Example(""" + struct PreferencesView: View { + var body: some View { + VStack { + ↓Image(nsImage: NSImage(named: "gear") ?? NSImage()) + .resizable() + .frame(width: 24, height: 24) + Text("Settings") + } + } + } + """), + Example(""" + struct FaviconView: View { + let favicon: UIImage? + var body: some View { + ↓Image(uiImage: favicon ?? UIImage()) + .aspectRatio(contentMode: .fit) + .frame(width: 16, height: 16) + } + } + """), + Example(""" + struct IconGrid: View { + var body: some View { + HStack { + ↓Image(uiImage: loadedImage) + .resizable() + ↓Image(systemName: "star.fill") + .foregroundColor(.yellow) + }.accessibilityElement(children: .combine) + } + } + """), + Example(""" + struct CardView: View { + var body: some View { + VStack { + ↓Image(uiImage: backgroundImage) + .resizable() + .aspectRatio(contentMode: .fill) + Text("Card Content") + }.accessibilityElement(children: .contain) + } + } + """), ] } From 6650ebdd87b328bb4284b1b0015a308060ccea97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 21 Jun 2025 20:45:13 +0200 Subject: [PATCH 34/80] Put contributors on a new line --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 959a24dc67..866496672a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * `closure_end_indentation` * `file_length` * `vertical_whitespace` + [JP Simard](https://github.com/jpsim) [Matt Pennig](https://github.com/pennig) From 4ecae5b25244fe9cfb80173aeaef95e12f68794d Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 14:47:19 -0400 Subject: [PATCH 35/80] Migrate LineLengthRule from SourceKit to SwiftSyntax (#6111) Convert LineLengthRule to use SwiftSyntax instead of SourceKit for improved performance and better detection accuracy. The SwiftSyntax implementation: - Uses ViolationsSyntaxVisitor pattern with line-based validation - Pre-computes ignored lines using helper visitors for efficiency - Implements pure SwiftSyntax comment detection without SourceKit - Correctly handles function declarations, multiline strings - Maintains all configuration options including URL stripping - Preserves exact line position reporting for violations --- CHANGELOG.md | 1 + .../Rules/Metrics/LineLengthRule.swift | 302 +++++++++++------- 2 files changed, 186 insertions(+), 117 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866496672a..35660e3208 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * `accessibility_trait_for_button` * `closure_end_indentation` * `file_length` + * `line_length` * `vertical_whitespace` [JP Simard](https://github.com/jpsim) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift index 38035d0992..fa35cfface 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift @@ -1,13 +1,11 @@ import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax +@SwiftSyntaxRule struct LineLengthRule: Rule { var configuration = LineLengthConfiguration() - private let commentKinds = SyntaxKind.commentKinds - private let nonCommentKinds = SyntaxKind.allKinds.subtracting(SyntaxKind.commentKinds) - private let functionKinds = SwiftDeclarationKind.functionKinds - static let description = RuleDescription( identifier: "line_length", name: "Line Length", @@ -24,149 +22,219 @@ struct LineLengthRule: Rule { Example(String(repeating: "#imageLiteral(resourceName: \"image.jpg\")", count: 121) + ""), ].skipWrappingInCommentTests().skipWrappingInStringTests() ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - let minValue = configuration.params.map(\.value).min() ?? .max - let swiftDeclarationKindsByLine = Lazy(file.swiftDeclarationKindsByLine() ?? []) - let syntaxKindsByLine = Lazy(file.syntaxKindsByLine() ?? []) - - return file.lines.compactMap { line in - // `line.content.count` <= `line.range.length` is true. - // So, `check line.range.length` is larger than minimum parameter value. - // for avoiding using heavy `line.content.count`. - if line.range.length < minValue { - return nil - } +private extension LineLengthRule { + final class Visitor: ViolationsSyntaxVisitor { + // To store line numbers that should be ignored based on configuration + private var functionDeclarationLines = Set() + private var commentOnlyLines = Set() + private var interpolatedStringLines = Set() + private var multilineStringLines = Set() - if configuration.ignoresFunctionDeclarations && - lineHasKinds(line: line, - kinds: functionKinds, - kindsByLine: swiftDeclarationKindsByLine.value) { - return nil + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + // Populate functionDeclarationLines if ignores_function_declarations is true + if configuration.ignoresFunctionDeclarations { + let funcVisitor = FunctionLineVisitor(locationConverter: locationConverter) + functionDeclarationLines = funcVisitor.walk(tree: node, handler: \.lines) } - if configuration.ignoresComments && - lineHasKinds(line: line, - kinds: commentKinds, - kindsByLine: syntaxKindsByLine.value) && - !lineHasKinds(line: line, - kinds: nonCommentKinds, - kindsByLine: syntaxKindsByLine.value) { - return nil + // Populate multilineStringLines if ignores_multiline_strings is true + if configuration.ignoresMultilineStrings { + let stringVisitor = MultilineStringLiteralVisitor(locationConverter: locationConverter) + multilineStringLines = stringVisitor.walk(tree: node, handler: \.linesSpanned) } - if configuration.ignoresInterpolatedStrings && - lineHasKinds(line: line, - kinds: [.stringInterpolationAnchor], - kindsByLine: syntaxKindsByLine.value) { - return nil + // Populate interpolatedStringLines if ignores_interpolated_strings is true + if configuration.ignoresInterpolatedStrings { + let interpVisitor = InterpolatedStringLineVisitor(locationConverter: locationConverter) + interpolatedStringLines = interpVisitor.walk(tree: node, handler: \.lines) } - if configuration.ignoresMultilineStrings && - lineIsMultilineString(line, file: file, syntaxKindsByLine: syntaxKindsByLine.value) { - return nil + // Populate commentOnlyLines if ignores_comments is true + if configuration.ignoresComments { + commentOnlyLines = findCommentOnlyLines(in: node, file: file, locationConverter: locationConverter) } - for pattern in configuration.excludedLinesPatterns where line.containsMatchingPattern(pattern) { - return nil + return .skipChildren // We'll do the main processing in visitPost + } + + override func visitPost(_: SourceFileSyntax) { + let minLengthThreshold = configuration.params.map(\.value).min() ?? .max + + for line in file.lines { + // Quick check to skip very short lines before expensive stripping + // `line.content.count` <= `line.range.length` is true. + // So, check `line.range.length` is larger than minimum parameter value + // for avoiding using heavy `line.content.count`. + if line.range.length < minLengthThreshold { + continue + } + + // Apply ignore configurations + if configuration.ignoresFunctionDeclarations && functionDeclarationLines.contains(line.index) { + continue + } + if configuration.ignoresComments && commentOnlyLines.contains(line.index) { + continue + } + if configuration.ignoresInterpolatedStrings && interpolatedStringLines.contains(line.index) { + continue + } + if configuration.ignoresMultilineStrings && multilineStringLines.contains(line.index) { + continue + } + if configuration.excludedLinesPatterns.contains(where: { + regex($0).firstMatch(in: line.content, range: line.content.fullNSRange) != nil + }) { + continue + } + + // String stripping logic + var strippedString = line.content + if configuration.ignoresURLs { + strippedString = strippedString.strippingURLs + } + strippedString = stripLiterals(fromSourceString: strippedString, withDelimiter: "#colorLiteral") + strippedString = stripLiterals(fromSourceString: strippedString, withDelimiter: "#imageLiteral") + + let length = strippedString.count // Character count for reporting + + // Check against configured length limits + for param in configuration.params where length > param.value { + let reason = "Line should be \(param.value) characters or less; " + + "currently it has \(length) characters" + // Position the violation at the start of the line, consistent with original behavior + violations.append(ReasonedRuleViolation( + position: locationConverter.position(ofLine: line.index, column: 1), // Start of the line + reason: reason, + severity: param.severity + )) + break // Only report one violation (the most severe one reached) per line + } } + } - var strippedString = line.content - if configuration.ignoresURLs { - strippedString = strippedString.strippingURLs + // Strip color and image literals from the source string + private func stripLiterals(fromSourceString sourceString: String, + withDelimiter delimiter: String) -> String { + var modifiedString = sourceString + while modifiedString.contains("\(delimiter)(") { + if let rangeStart = modifiedString.range(of: "\(delimiter)("), + let rangeEnd = modifiedString.range(of: ")", options: .literal, + range: rangeStart.lowerBound.. param.value { - let reason = "Line should be \(param.value) characters or less; currently it has \(length) characters" - return StyleViolation(ruleDescription: Self.description, - severity: param.severity, - location: Location(file: file.path, line: line.index), - reason: reason) + return modifiedString + } + + private func findCommentOnlyLines( + in node: SourceFileSyntax, + file: SwiftLintFile, + locationConverter: SourceLocationConverter + ) -> Set { + var commentOnlyLines = Set() + + // For each line, check if it contains only comments and whitespace + for line in file.lines { + let lineContent = line.content.trimmingCharacters(in: .whitespaces) + + // Skip empty lines + if lineContent.isEmpty { continue } + + // Check if line starts with comment markers + if lineContent.hasPrefix("//") || lineContent.hasPrefix("/*") || + (lineContent.hasPrefix("*/") && lineContent.count == 2) { + // Now verify using SwiftSyntax that this line doesn't contain any tokens + var hasNonCommentContent = false + + for token in node.tokens(viewMode: .sourceAccurate) { + if token.tokenKind == .endOfFile { continue } + + let tokenLine = locationConverter.location(for: token.position).line + if tokenLine == line.index { + hasNonCommentContent = true + break + } + } + + if !hasNonCommentContent { + commentOnlyLines.insert(line.index) + } + } } - return nil + + return commentOnlyLines } } +} - /// Checks if the given line is part of a multiline string - /// - Example: - /// ``` - /// let a = """ - /// - /// """ - /// ``` - private func lineIsMultilineString(_ line: Line, file: SwiftLintFile, syntaxKindsByLine: [[SyntaxKind]]) -> Bool { - // contents of multiline strings only include one string element per line - guard syntaxKindsByLine[line.index] == [.string] else { return false } - - // find the trailing delimiter `"""` in order to make sure we're not in a list of concatenated strings - let lastStringLineIndex = syntaxKindsByLine.dropFirst(line.index + 1).firstIndex(where: { $0 != [.string] }) - guard let lastStringLineIndex else { - return file.lines.last?.content.trimmingCharacters(in: .whitespaces).hasPrefix("\"\"\"") == true - } +// MARK: - Helper Visitors for Pre-computation - // lines include leading empty element - // check last string line for single `"""` - // and if it fails, check the next line contains more than just a string for `"""; let a = 1` - return file.lines[lastStringLineIndex - 1].content.trimmingCharacters(in: .whitespaces).hasPrefix("\"\"\"") || - file.lines[lastStringLineIndex - 2].content.trimmingCharacters(in: .whitespaces).hasPrefix("\"\"\"") +// Visitor to find lines spanned by function declarations +private final class FunctionLineVisitor: SyntaxVisitor { + let locationConverter: SourceLocationConverter + var lines = Set() + + init(locationConverter: SourceLocationConverter) { + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } - /// Takes a string and replaces any literals specified by the `delimiter` parameter with `#` - /// - /// - parameter sourceString: Original string, possibly containing literals - /// - parameter delimiter: Delimiter of the literal - /// (characters before the parentheses, e.g. `#colorLiteral`) - /// - /// - returns: sourceString with the given literals replaced by `#` - private func stripLiterals(fromSourceString sourceString: String, - withDelimiter delimiter: String) -> String { - var modifiedString = sourceString - - // While copy of content contains literal, replace with a single character - while modifiedString.contains("\(delimiter)(") { - if let rangeStart = modifiedString.range(of: "\(delimiter)("), - let rangeEnd = modifiedString.range(of: ")", - options: .literal, - range: - rangeStart.lowerBound..() + + init(locationConverter: SourceLocationConverter) { + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } - private func lineHasKinds(line: Line, kinds: Set, kindsByLine: [[Kind]]) -> Bool { - let index = line.index - if index >= kindsByLine.count { - return false + override func visitPost(_ node: ExpressionSegmentSyntax) { + // ExpressionSegmentSyntax is the interpolation inside a string + let startLocation = locationConverter.location(for: node.positionAfterSkippingLeadingTrivia) + let endLocation = locationConverter.location(for: node.endPositionBeforeTrailingTrivia) + for line in startLocation.line...endLocation.line { + lines.insert(line) } - return !kinds.isDisjoint(with: kindsByLine[index]) } } -private extension Line { - func containsMatchingPattern(_ pattern: String) -> Bool { - regex(pattern).firstMatch(in: content, range: content.fullNSRange) != nil +// Visitor to find line ranges covered by multiline string literals +private final class MultilineStringLiteralVisitor: SyntaxVisitor { + let locationConverter: SourceLocationConverter + var linesSpanned = Set() + + init(locationConverter: SourceLocationConverter) { + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } -} -// extracted from https://forums.swift.org/t/pitch-declaring-local-variables-as-lazy/9287/3 -private class Lazy { - private var computation: () -> Result - fileprivate private(set) lazy var value: Result = computation() + override func visitPost(_ node: StringLiteralExprSyntax) { + guard node.openingQuote.tokenKind == .multilineStringQuote || + (node.openingPounds != nil && node.openingQuote.tokenKind == .stringQuote) else { + return + } + + let startLocation = locationConverter.location(for: node.positionAfterSkippingLeadingTrivia) + let endLocation = locationConverter.location(for: node.endPositionBeforeTrailingTrivia) - init(_ computation: @escaping @autoclosure () -> Result) { - self.computation = computation + for line in startLocation.line...endLocation.line { + linesSpanned.insert(line) + } } } From 5a2cf4b1fe3a99ebc5ee65850c1d21a4449f8629 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 15:19:37 -0400 Subject: [PATCH 36/80] Remove dead code (#6125) In particular lots of stuff that used to be needed with SourceKit that we no longer need to keep around. Identified using Periphery: https://github.com/peripheryapp/periphery --- .../SourceKittenDictionary+SwiftUI.swift | 186 ------------------ .../AccessibilityTraitForButtonRule.swift | 9 - .../Rules/Lint/MarkRule.swift | 1 - .../DeploymentTargetConfiguration.swift | 4 - .../Rules/Style/IdentifierNameRule.swift | 24 --- .../Rules/Style/TypeContentsOrderRule.swift | 2 - .../Extensions/Dictionary+SwiftLint.swift | 17 -- .../SourceKittenDictionary+Swiftlint.swift | 15 -- .../SwiftDeclarationKind+SwiftLint.swift | 17 -- .../Extensions/SwiftLintFile+Regex.swift | 31 --- .../Models/AccessControlLevel.swift | 5 - .../Models/SwiftLintSyntaxMap.swift | 4 - .../LintableFilesVisitor.swift | 10 - .../Models/YamlParser.swift | 6 +- .../OpeningBraceRuleTests.swift | 6 - Tests/TestHelpers/SwiftLintTestCase.swift | 52 ----- 16 files changed, 3 insertions(+), 386 deletions(-) delete mode 100644 Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift diff --git a/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift b/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift deleted file mode 100644 index 088917375e..0000000000 --- a/Source/SwiftLintBuiltInRules/Extensions/SourceKittenDictionary+SwiftUI.swift +++ /dev/null @@ -1,186 +0,0 @@ -import SourceKittenFramework - -/// Struct to represent SwiftUI ViewModifiers for the purpose of finding modifiers in a substructure. -struct SwiftUIModifier { - /// Name of the modifier. - let name: String - - /// List of arguments to check for in the modifier. - let arguments: [Argument] - - struct Argument { - /// Name of the argument we want to find. For single unnamed arguments, use the empty string. - let name: String - - /// Whether or not the argument is required. If the argument is present, value checks are enforced. - /// Allows for better handling of modifiers with default values for certain arguments where we want - /// to ensure that the default value is used. - let required: Bool - - /// List of possible values for the argument. Typically should just be a list with a single element, - /// but allows for the flexibility of checking for multiple possible values. To only check for the presence - /// of the modifier and not enforce any certain values, pass an empty array. All values are parsed as - /// Strings; for other types (boolean, numeric, optional, etc) types you can check for "true", "5", "nil", etc. - let values: [String] - - /// Success criteria used for matching values (prefix, suffix, substring, exact match, or none). - let matchType: MatchType - - init(name: String, required: Bool = true, values: [String], matchType: MatchType = .exactMatch) { - self.name = name - self.required = required - self.values = values - self.matchType = matchType - } - } - - enum MatchType { - case prefix, suffix, substring, exactMatch - - /// Compares the parsed argument value to a target value for the given match type - /// and returns true is a match is found. - func matches(argumentValue: String, targetValue: String) -> Bool { - switch self { - case .prefix: - return argumentValue.hasPrefix(targetValue) - case .suffix: - return argumentValue.hasSuffix(targetValue) - case .substring: - return argumentValue.contains(targetValue) - case .exactMatch: - return argumentValue == targetValue - } - } - } -} - -/// Extensions for recursively checking SwiftUI code for certain modifiers. -extension SourceKittenDictionary { - /// Call on a SwiftUI View to recursively check the substructure for a certain modifier with certain arguments. - /// - Parameters: - /// - modifiers: A list of `SwiftUIModifier` structs to check for in the view's substructure. - /// In most cases, this can just be a single modifier, but since some modifiers have - /// multiple versions, this enables checking for any modifier from the list. - /// - file: The SwiftLintFile object for the current file, used to extract argument values. - /// - Returns: A boolean value representing whether or not the given modifier with the specified - /// arguments appears in the view's substructure. - func hasModifier(anyOf modifiers: [SwiftUIModifier], in file: SwiftLintFile) -> Bool { - // SwiftUI ViewModifiers are treated as `call` expressions, and we make sure we can get the expression's name. - guard expressionKind == .call, let name else { - return false - } - - // If any modifier from the list matches, return true. - for modifier in modifiers { - // Check for the given modifier name - guard name.hasSuffix(modifier.name) else { - continue - } - - // Check arguments. - var matchesArgs = true - for argument in modifier.arguments { - var foundArg = false - var argValue: String? - - // Check for single unnamed argument. - if argument.name.isEmpty { - foundArg = true - argValue = getSingleUnnamedArgumentValue(in: file) - } else if let parsedArgument = enclosedArguments.first(where: { $0.name == argument.name }) { - foundArg = true - argValue = parsedArgument.getArgumentValue(in: file) - } - - // If argument is not required and we didn't find it, continue. - if !foundArg && !argument.required { - continue - } - - // Otherwise, we must have found an argument with a non-nil value to continue. - guard foundArg, let argumentValue = argValue else { - matchesArgs = false - break - } - - // Argument value can match any of the options given in the argument struct. - if argument.values.isEmpty || argument.values.contains(where: { - argument.matchType.matches(argumentValue: argumentValue, targetValue: $0) - }) { - // Found a match, continue to next argument. - continue - } - // Did not find a match, exit loop over arguments. - matchesArgs = false - break - } - - // Return true if all arguments matched - if matchesArgs { - return true - } - } - - // Recursively check substructure. - // SwiftUI literal Views with modifiers will have a SourceKittenDictionary structure like: - // Image("myImage").resizable().accessibility(hidden: true).frame - // --> Image("myImage").resizable().accessibility - // --> Image("myImage").resizable - // --> Image - return substructure.contains(where: { $0.hasModifier(anyOf: modifiers, in: file) }) - } - - // MARK: Sample use cases of `hasModifier` that are used in multiple rules - - /// Whether or not the dictionary represents a SwiftUI View with an `accesibilityHidden(true)` - /// or `accessibility(hidden: true)` modifier. - func hasAccessibilityHiddenModifier(in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityHidden", - arguments: [.init(name: "", values: ["true"])] - ), - SwiftUIModifier( - name: "accessibility", - arguments: [.init(name: "hidden", values: ["true"])] - ), - ], - in: file - ) - } - - /// Whether or not the dictionary represents a SwiftUI View with an `accessibilityElement()` or - /// `accessibilityElement(children: .ignore)` modifier (`.ignore` is the default parameter value). - func hasAccessibilityElementChildrenIgnoreModifier(in file: SwiftLintFile) -> Bool { - hasModifier( - anyOf: [ - SwiftUIModifier( - name: "accessibilityElement", - arguments: [.init(name: "children", required: false, values: [".ignore"], matchType: .suffix)] - ), - ], - in: file - ) - } - - // MARK: Helpers to extract argument values - - /// Helper to get the value of an argument. - func getArgumentValue(in file: SwiftLintFile) -> String? { - guard expressionKind == .argument, let bodyByteRange else { - return nil - } - - return file.stringView.substringWithByteRange(bodyByteRange) - } - - /// Helper to get the value of a single unnamed argument to a function call. - func getSingleUnnamedArgumentValue(in file: SwiftLintFile) -> String? { - guard expressionKind == .call, let bodyByteRange else { - return nil - } - - return file.stringView.substringWithByteRange(bodyByteRange) - } -} diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift index 11176fbbf2..e4bbbcdba9 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift @@ -273,13 +273,4 @@ private extension FunctionCallExprSyntax { return false } - - func isModifierChainRoot() -> Bool { - // Check if this function call is at the root of a modifier chain - // (i.e., it's the topmost expression in a chain like Text().modifier1().modifier2()) - guard let memberAccess = calledExpression.as(MemberAccessExprSyntax.self) else { - return false // Direct function calls like Text() are not modifier chain roots - } - return memberAccess.base != nil // Has a base, so it's part of a modifier chain - } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift index d198393897..a81026a6b0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MarkRule.swift @@ -78,7 +78,6 @@ private extension TokenSyntax { ].map(basePattern).joined(separator: "|")) + capturingGroup(hyphenOrEmpty) private static let anySpace = " *" - private static let nonSpaceOrTwoOrMoreSpace = "(?: {2,})?" private static let anyText = "(?:\\S.*)" diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift index 6c7cd87ee1..90413da9b2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift @@ -52,10 +52,6 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { self.init(platform: platform, major: major, minor: minor, patch: patch) } - var configurationKey: String { - platform.configurationKey - } - private static func parseVersion(string: String) throws -> (Int, Int, Int) { func parseNumber(_ string: String) throws -> Int { guard let number = Int(string) else { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift index 8b4564ea78..ee00bdfa5b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IdentifierNameRule.swift @@ -146,12 +146,6 @@ private extension IdentifierNameRule { } } -private extension DeclModifierListSyntax { - var staticOrClassModifier: DeclModifierSyntax? { - first { ["static", "class"].contains($0.name.text) } - } -} - private extension IdentifierPatternSyntax { var enclosingVarDecl: VariableDeclSyntax? { let identifierDecl = @@ -171,24 +165,6 @@ private extension IdentifierPatternSyntax { } } -private extension VariableDeclSyntax { - var allDeclaredNames: [String] { - bindings - .map(\.pattern) - .flatMap { pattern -> [String] in - if let id = pattern.as(IdentifierPatternSyntax.self) { - [id.identifier.text] - } else if let tuple = pattern.as(TuplePatternSyntax.self) { - tuple.elements.compactMap { - $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text - } - } else { - [] - } - } - } -} - private enum NamedDeclType: CustomStringConvertible { case function(name: String, resolvedName: String, isPrivate: Bool) case enumElement(name: String) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift index 54a42b5c64..cf2bbd3dad 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TypeContentsOrderRule.swift @@ -3,8 +3,6 @@ import SwiftSyntax @SwiftSyntaxRule(optIn: true) struct TypeContentsOrderRule: Rule { - private typealias TypeContentOffset = (typeContent: TypeContent, offset: ByteCount) - var configuration = TypeContentsOrderConfiguration() static let description = RuleDescription( diff --git a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift index c9221ccebf..ecd8a6d029 100644 --- a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift @@ -46,12 +46,6 @@ public struct SourceKittenDictionary { (value["key.bodyoffset"] as? Int64).map(ByteCount.init) } - /// Body byte range. - public var bodyByteRange: ByteRange? { - guard let offset = bodyOffset, let length = bodyLength else { return nil } - return ByteRange(location: offset, length: length) - } - /// Kind. public var kind: String? { value["key.kind"] as? String @@ -76,12 +70,6 @@ public struct SourceKittenDictionary { (value["key.nameoffset"] as? Int64).map(ByteCount.init) } - /// Byte range of name. - public var nameByteRange: ByteRange? { - guard let offset = nameOffset, let length = nameLength else { return nil } - return ByteRange(location: offset, length: length) - } - /// Offset. public var offset: ByteCount? { (value["key.offset"] as? Int64).map(ByteCount.init) @@ -103,11 +91,6 @@ public struct SourceKittenDictionary { value["key.typename"] as? String } - /// Documentation length. - public var docLength: ByteCount? { - (value["key.doclength"] as? Int64).flatMap(ByteCount.init) - } - /// The attribute for this dictionary, as returned by SourceKit. public var attribute: String? { value["key.attribute"] as? String diff --git a/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift b/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift index dff42f5f20..edfd2c8498 100644 --- a/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift +++ b/Source/SwiftLintCore/Extensions/SourceKittenDictionary+Swiftlint.swift @@ -27,21 +27,6 @@ public extension SourceKittenDictionary { return results } - func structures(forByteOffset byteOffset: ByteCount) -> [SourceKittenDictionary] { - var results = [SourceKittenDictionary]() - - func parse(_ dictionary: SourceKittenDictionary) { - guard let byteRange = dictionary.byteRange, byteRange.contains(byteOffset) else { - return - } - - results.append(dictionary) - dictionary.substructure.forEach(parse) - } - parse(self) - return results - } - /// Return the string content of this structure in the given file. /// - Parameter file: File this structure occurs in /// - Returns: The content of the file which this `SourceKittenDictionary` structure represents diff --git a/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift index 2e99c696ea..c3afbc8510 100644 --- a/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftDeclarationKind+SwiftLint.swift @@ -1,15 +1,6 @@ @preconcurrency import SourceKittenFramework public extension SwiftDeclarationKind { - static let variableKinds: Set = [ - .varClass, - .varGlobal, - .varInstance, - .varLocal, - .varParameter, - .varStatic, - ] - static let functionKinds: Set = [ .functionAccessorAddress, .functionAccessorDidset, @@ -34,12 +25,4 @@ public extension SwiftDeclarationKind { .associatedtype, .enum, ] - - static let extensionKinds: Set = [ - .extension, - .extensionClass, - .extensionEnum, - .extensionProtocol, - .extensionStruct, - ] } diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift index 1f4ce74a43..a8e896de99 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Regex.swift @@ -117,11 +117,6 @@ extension SwiftLintFile { } } - public func rangesAndTokens(matching pattern: String, - range: NSRange? = nil) -> [(NSRange, [SwiftLintSyntaxToken])] { - matchesAndTokens(matching: pattern, range: range).map { ($0.0.range, $0.1) } - } - public func match(pattern: String, range: NSRange? = nil, captureGroup: Int = 0) -> [(NSRange, [SyntaxKind])] { matchesAndSyntaxKinds(matching: pattern, range: range).map { textCheckingResult, syntaxKinds in (textCheckingResult.range(at: captureGroup), syntaxKinds) @@ -208,23 +203,6 @@ extension SwiftLintFile { .map(\.0) } - public typealias MatchMapping = (NSTextCheckingResult) -> NSRange - - public func match(pattern: String, - range: NSRange? = nil, - excludingSyntaxKinds: Set, - excludingPattern: String, - exclusionMapping: MatchMapping = \.range) -> [NSRange] { - let matches = match(pattern: pattern, excludingSyntaxKinds: excludingSyntaxKinds) - if matches.isEmpty { - return [] - } - let range = range ?? stringView.range - let exclusionRanges = regex(excludingPattern).matches(in: stringView, options: [], - range: range).map(exclusionMapping) - return matches.filter { !$0.intersects(exclusionRanges) } - } - public func append(_ string: String) { guard string.isNotEmpty else { return @@ -282,15 +260,6 @@ extension SwiftLintFile { ruleEnabled(violatingRanges: [violatingRange], for: rule).first } - public func isACL(token: SwiftLintSyntaxToken) -> Bool { - guard token.kind == .attributeBuiltin else { - return false - } - - let aclString = contents(for: token) - return aclString.flatMap(AccessControlLevel.init(description:)) != nil - } - public func contents(for token: SwiftLintSyntaxToken) -> String? { stringView.substringWithByteRange(token.range) } diff --git a/Source/SwiftLintCore/Models/AccessControlLevel.swift b/Source/SwiftLintCore/Models/AccessControlLevel.swift index 160956d0b8..11b1729a76 100644 --- a/Source/SwiftLintCore/Models/AccessControlLevel.swift +++ b/Source/SwiftLintCore/Models/AccessControlLevel.swift @@ -47,11 +47,6 @@ public enum AccessControlLevel: String, CustomStringConvertible { case .open: return "open" } } - - /// Returns true if is `private` or `fileprivate` - public var isPrivate: Bool { - self == .private || self == .fileprivate - } } extension AccessControlLevel: Comparable { diff --git a/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift b/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift index 96d5f5e1fc..99214dbfda 100644 --- a/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift +++ b/Source/SwiftLintCore/Models/SwiftLintSyntaxMap.swift @@ -2,9 +2,6 @@ import SourceKittenFramework /// Represents a Swift file's syntax information. public struct SwiftLintSyntaxMap { - /// The raw `SyntaxMap` obtained by SourceKitten. - public let value: SyntaxMap - /// The SwiftLint-specific syntax tokens for this syntax map. public let tokens: [SwiftLintSyntaxToken] @@ -12,7 +9,6 @@ public struct SwiftLintSyntaxMap { /// /// - parameter value: The raw `SyntaxMap` obtained by SourceKitten. public init(value: SyntaxMap) { - self.value = value self.tokens = value.tokens.map(SwiftLintSyntaxToken.init) } diff --git a/Source/SwiftLintFramework/LintableFilesVisitor.swift b/Source/SwiftLintFramework/LintableFilesVisitor.swift index c6c630e335..d18ba025f7 100644 --- a/Source/SwiftLintFramework/LintableFilesVisitor.swift +++ b/Source/SwiftLintFramework/LintableFilesVisitor.swift @@ -57,16 +57,6 @@ enum LintOrAnalyzeModeWithCompilerArguments { case analyze(allCompilerInvocations: CompilerInvocations) } -private func resolveParamsFiles(args: [String]) -> [String] { - args.reduce(into: []) { (allArgs: inout [String], arg: String) in - if arg.hasPrefix("@"), let contents = try? String(contentsOfFile: String(arg.dropFirst())) { - allArgs.append(contentsOf: resolveParamsFiles(args: contents.split(separator: "\n").map(String.init))) - } else { - allArgs.append(arg) - } - } -} - struct LintableFilesVisitor { let options: LintOrAnalyzeOptions let cache: LinterCache? diff --git a/Source/SwiftLintFramework/Models/YamlParser.swift b/Source/SwiftLintFramework/Models/YamlParser.swift index 018478a927..da44759271 100644 --- a/Source/SwiftLintFramework/Models/YamlParser.swift +++ b/Source/SwiftLintFramework/Models/YamlParser.swift @@ -4,7 +4,7 @@ import Yams // MARK: - YamlParser /// An interface for parsing YAML. -public struct YamlParser { +struct YamlParser { /// Parses the input YAML string as an untyped dictionary. /// /// - parameter yaml: YAML-formatted string. @@ -13,8 +13,8 @@ public struct YamlParser { /// - returns: The parsed YAML as an untyped dictionary. /// /// - throws: Throws if the `yaml` string provided could not be parsed. - public static func parse(_ yaml: String, - env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] { + static func parse(_ yaml: String, + env: [String: String] = ProcessInfo.processInfo.environment) throws -> [String: Any] { do { return try Yams.load(yaml: yaml, .default, .swiftlintConstructor(env: env)) as? [String: Any] ?? [:] diff --git a/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift b/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift index fe04b07fda..a2246f8261 100644 --- a/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift +++ b/Tests/BuiltInRulesTests/OpeningBraceRuleTests.swift @@ -193,9 +193,3 @@ final class OpeningBraceRuleTests: SwiftLintTestCase { verifyRule(description, ruleConfiguration: ["ignore_multiline_function_signatures": true]) } } - -private extension Array where Element == Example { - func removing(_ examples: Self) -> Self { - filter { !examples.contains($0) } - } -} diff --git a/Tests/TestHelpers/SwiftLintTestCase.swift b/Tests/TestHelpers/SwiftLintTestCase.swift index 3a99c513fd..ad8b591e3b 100644 --- a/Tests/TestHelpers/SwiftLintTestCase.swift +++ b/Tests/TestHelpers/SwiftLintTestCase.swift @@ -1,62 +1,10 @@ import SwiftLintFramework import XCTest -// swiftlint:disable:next blanket_disable_command -// swiftlint:disable test_case_accessibility - // swiftlint:disable:next balanced_xctest_lifecycle open class SwiftLintTestCase: XCTestCase { override open class func setUp() { super.setUp() RuleRegistry.registerAllRulesOnce() } - - // swiftlint:disable:next identifier_name - public func AsyncAssertFalse(_ condition: @autoclosure () async -> Bool, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let condition = await condition() - XCTAssertFalse(condition, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertTrue(_ condition: @autoclosure () async throws -> Bool, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async rethrows { - let condition = try await condition() - XCTAssertTrue(condition, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertEqual(_ expression1: @autoclosure () async throws -> T, - _ expression2: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async rethrows { - let value1 = try await expression1() - let value2 = try await expression2() - XCTAssertEqual(value1, value2, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertNotEqual(_ expression1: @autoclosure () async -> T, - _ expression2: @autoclosure () async -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let value1 = await expression1() - let value2 = await expression2() - XCTAssertNotEqual(value1, value2, message(), file: file, line: line) - } - - // swiftlint:disable:next identifier_name - public func AsyncAssertNil(_ expression: @autoclosure () async -> T?, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let value = await expression() - XCTAssertNil(value, message(), file: file, line: line) - } } From d22e7335abc7d3bade1ac5d44cc775638e41688a Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 15:27:21 -0400 Subject: [PATCH 37/80] Add `SwiftSyntaxKindBridge` to help migrate custom rules to SwiftSyntax (#6126) This provides an alternative to getting syntax kinds from SourceKit. The mappings aren't 100% equivalent, but this should serve as a useful compatibility layer. --- .../Extensions/SwiftLintFile+Cache.swift | 10 + .../Helpers/SwiftSyntaxKindBridge.swift | 61 +++++++ .../SwiftSyntaxKindBridgeTests.swift | 171 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift create mode 100644 Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift diff --git a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift index 61a5d63302..85aeb8fb77 100644 --- a/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift +++ b/Source/SwiftLintCore/Extensions/SwiftLintFile+Cache.swift @@ -49,6 +49,10 @@ private let syntaxClassificationsCache = Cache { $0.syntaxTree.classifications } private let syntaxKindsByLinesCache = Cache { $0.syntaxKindsByLine() } private let syntaxTokensByLinesCache = Cache { $0.syntaxTokensByLine() } private let linesWithTokensCache = Cache { $0.computeLinesWithTokens() } +private let swiftSyntaxTokensCache = Cache { file -> [SwiftLintSyntaxToken]? in + // Use SwiftSyntaxKindBridge to derive SourceKitten-compatible tokens from SwiftSyntax + SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) +} package typealias AssertHandler = () -> Void // Re-enable once all parser diagnostics in tests have been addressed. @@ -190,6 +194,10 @@ extension SwiftLintFile { return syntaxKindsByLines } + public var swiftSyntaxDerivedSourceKittenTokens: [SwiftLintSyntaxToken]? { + swiftSyntaxTokensCache.get(self) + } + /// Invalidates all cached data for this file. public func invalidateCache() { file.clearCaches() @@ -200,6 +208,7 @@ extension SwiftLintFile { syntaxMapCache.invalidate(self) syntaxTokensByLinesCache.invalidate(self) syntaxKindsByLinesCache.invalidate(self) + swiftSyntaxTokensCache.invalidate(self) syntaxTreeCache.invalidate(self) foldedSyntaxTreeCache.invalidate(self) locationConverterCache.invalidate(self) @@ -215,6 +224,7 @@ extension SwiftLintFile { syntaxMapCache.clear() syntaxTokensByLinesCache.clear() syntaxKindsByLinesCache.clear() + swiftSyntaxTokensCache.clear() syntaxTreeCache.clear() foldedSyntaxTreeCache.clear() locationConverterCache.clear() diff --git a/Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift b/Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift new file mode 100644 index 0000000000..637930d2ff --- /dev/null +++ b/Source/SwiftLintCore/Helpers/SwiftSyntaxKindBridge.swift @@ -0,0 +1,61 @@ +import SourceKittenFramework +import SwiftIDEUtils +import SwiftSyntax + +/// Bridge to convert SwiftSyntax classifications to SourceKitten syntax kinds. +/// This enables SwiftSyntax-based custom rules to work with kind filtering +/// without making any SourceKit calls. +public enum SwiftSyntaxKindBridge { + /// Map a SwiftSyntax classification to SourceKitten syntax kind. + static func mapClassification(_ classification: SyntaxClassification) -> SourceKittenFramework.SyntaxKind? { + // swiftlint:disable:previous cyclomatic_complexity + switch classification { + case .attribute: + return .attributeID + case .blockComment, .lineComment: + return .comment + case .docBlockComment, .docLineComment: + return .docComment + case .dollarIdentifier, .identifier: + return .identifier + case .editorPlaceholder: + return .placeholder + case .floatLiteral, .integerLiteral: + return .number + case .ifConfigDirective: + return .poundDirectiveKeyword + case .keyword: + return .keyword + case .none, .regexLiteral: + return nil + case .operator: + return .operator + case .stringLiteral: + return .string + case .type: + return .typeidentifier + case .argumentLabel: + return .argument + @unknown default: + return nil + } + } + + /// Convert SwiftSyntax syntax classifications to SourceKitten-compatible syntax tokens. + public static func sourceKittenSyntaxKinds(for file: SwiftLintFile) -> [SwiftLintSyntaxToken] { + file.syntaxClassifications.compactMap { classifiedRange in + guard let syntaxKind = mapClassification(classifiedRange.kind) else { + return nil + } + + let byteRange = classifiedRange.range.toSourceKittenByteRange() + let token = SyntaxToken( + type: syntaxKind.rawValue, + offset: byteRange.location, + length: byteRange.length + ) + + return SwiftLintSyntaxToken(value: token) + } + } +} diff --git a/Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift b/Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift new file mode 100644 index 0000000000..8fdb9da72a --- /dev/null +++ b/Tests/FrameworkTests/SwiftSyntaxKindBridgeTests.swift @@ -0,0 +1,171 @@ +import SourceKittenFramework +import SwiftIDEUtils +@testable import SwiftLintCore +import SwiftSyntax +import TestHelpers +import XCTest + +final class SwiftSyntaxKindBridgeTests: SwiftLintTestCase { + func testBasicKeywordMapping() { + // Test basic keyword mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.keyword), .keyword) + } + + func testIdentifierMapping() { + // Test identifier mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.identifier), .identifier) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.dollarIdentifier), .identifier) + } + + func testCommentMapping() { + // Test comment mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.lineComment), .comment) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.blockComment), .comment) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.docLineComment), .docComment) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.docBlockComment), .docComment) + } + + func testLiteralMapping() { + // Test literal mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.stringLiteral), .string) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.integerLiteral), .number) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.floatLiteral), .number) + } + + func testOperatorAndTypeMapping() { + // Test operator and type mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.operator), .operator) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.type), .typeidentifier) + } + + func testSpecialCaseMapping() { + // Test special case mappings + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.attribute), .attributeID) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.editorPlaceholder), .placeholder) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.ifConfigDirective), .poundDirectiveKeyword) + XCTAssertEqual(SwiftSyntaxKindBridge.mapClassification(.argumentLabel), .argument) + } + + func testUnmappedClassifications() { + // Test classifications that have no mapping + XCTAssertNil(SwiftSyntaxKindBridge.mapClassification(.none)) + XCTAssertNil(SwiftSyntaxKindBridge.mapClassification(.regexLiteral)) + } + + func testSourceKittenSyntaxKindsGeneration() { + // Test that we can generate SourceKitten-compatible tokens from a simple Swift file + let contents = """ + // This is a comment + let x = 42 + """ + let file = SwiftLintFile(contents: contents) + + // Get the tokens from the bridge + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + + // Verify we got some tokens + XCTAssertFalse(tokens.isEmpty) + + // Check that we have expected token types + let tokenTypes = Set(tokens.map { $0.value.type }) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.comment.rawValue)) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.keyword.rawValue)) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.identifier.rawValue)) + XCTAssertTrue(tokenTypes.contains(SyntaxKind.number.rawValue)) + } + + func testTokenOffsetAndLength() { + // Test that token offsets and lengths are correct + let contents = "let x = 42" + let file = SwiftLintFile(contents: contents) + + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + + // Find the "let" keyword token + let letToken = tokens.first { token in + if token.value.type == SyntaxKind.keyword.rawValue { + let start = token.value.offset.value + let end = token.value.offset.value + token.value.length.value + let startIndex = contents.index(contents.startIndex, offsetBy: start) + let endIndex = contents.index(contents.startIndex, offsetBy: end) + let substring = String(contents[startIndex.. Int { return value } + } + """ + let file = SwiftLintFile(contents: contents) + + // This should succeed without any fatal errors from the validation system + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + XCTAssertFalse(tokens.isEmpty) + } + + func testEmptyFileHandling() { + // Test that empty files are handled gracefully + let file = SwiftLintFile(contents: "") + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + XCTAssertTrue(tokens.isEmpty) + } + + func testWhitespaceOnlyFile() { + // Test files with only whitespace + let file = SwiftLintFile(contents: " \n\n \t \n") + let tokens = SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: file) + // Whitespace is not classified, so we should get no tokens + XCTAssertTrue(tokens.isEmpty) + } +} From 3a922d41f9211d1c5f4264d406af80b081580eb9 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sat, 21 Jun 2025 15:54:56 -0400 Subject: [PATCH 38/80] Add `ConditionallySourceKitFree` to migrate custom rules to SwiftSyntax (#6127) The protocol will be used to tag rules that may or may not require SourceKit depending on its configuration. I only expect this to be used for custom rules as utility to help transition to a fully SwiftSyntax based approach. --- .../Extensions/Request+SwiftLint.swift | 5 +- Source/SwiftLintCore/Protocols/Rule.swift | 26 ++++++ .../Documentation/RuleDocumentation.swift | 7 +- Source/SwiftLintFramework/Models/Linter.swift | 4 +- .../Rules/CustomRules.swift | 7 +- Source/swiftlint/Commands/Rules.swift | 2 +- .../ConditionallySourceKitFreeTests.swift | 87 +++++++++++++++++++ 7 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift diff --git a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift index 5d61b2750e..b9ce4aba07 100644 --- a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift @@ -19,7 +19,10 @@ public extension Request { } // Check if the current rule is a SourceKitFreeRule - if ruleType is any SourceKitFreeRule.Type { + // Skip check for ConditionallySourceKitFree rules since we can't determine + // at the type level if they're effectively SourceKit-free + if ruleType is any SourceKitFreeRule.Type && + !(ruleType is any ConditionallySourceKitFree.Type) { queuedFatalError(""" '\(ruleID)' is a SourceKitFreeRule and should not be making requests to SourceKit. """) diff --git a/Source/SwiftLintCore/Protocols/Rule.swift b/Source/SwiftLintCore/Protocols/Rule.swift index 94e36e6273..98427b4fb7 100644 --- a/Source/SwiftLintCore/Protocols/Rule.swift +++ b/Source/SwiftLintCore/Protocols/Rule.swift @@ -240,6 +240,32 @@ public extension SubstitutionCorrectableRule { /// A rule that does not need SourceKit to operate and can still operate even after SourceKit has crashed. public protocol SourceKitFreeRule: Rule {} +/// A rule that may or may not require SourceKit depending on its configuration. +public protocol ConditionallySourceKitFree: Rule { + /// Whether this rule is currently configured in a way that doesn't require SourceKit. + var isEffectivelySourceKitFree: Bool { get } +} + +public extension Rule { + /// Whether this rule requires SourceKit to operate. + /// Returns false if the rule conforms to SourceKitFreeRule or if it conforms to + /// ConditionallySourceKitFree and is currently configured to not require SourceKit. + var requiresSourceKit: Bool { + // Check if rule conforms to SourceKitFreeRule + if self is any SourceKitFreeRule { + return false + } + + // Check if rule is conditionally SourceKit-free and currently doesn't need SourceKit + if let conditionalRule = self as? any ConditionallySourceKitFree { + return !conditionalRule.isEffectivelySourceKitFree + } + + // All other rules require SourceKit + return true + } +} + /// A rule that can operate on the post-typechecked AST using compiler arguments. Performs rules that are more like /// static analysis than syntactic checks. public protocol AnalyzerRule: OptInRule {} diff --git a/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift b/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift index e80d72a439..157fb9fa73 100644 --- a/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift +++ b/Source/SwiftLintFramework/Documentation/RuleDocumentation.swift @@ -12,7 +12,12 @@ struct RuleDocumentation { var isLinterRule: Bool { !isAnalyzerRule } /// If this rule uses SourceKit. - var usesSourceKit: Bool { !(ruleType is any SourceKitFreeRule.Type) } + /// Note: For ConditionallySourceKitFree rules, this returns true since we can't + /// determine at the type level if they're effectively SourceKit-free. + var usesSourceKit: Bool { + !(ruleType is any SourceKitFreeRule.Type) || + (ruleType is any ConditionallySourceKitFree.Type) + } /// If this rule is disabled by default. var isDisabledByDefault: Bool { ruleType is any OptInRule.Type } diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index 41cfd64901..b61b88fc18 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -78,7 +78,9 @@ private extension Rule { return false } - if !(self is any SourceKitFreeRule) && file.sourcekitdFailed { + // Only check sourcekitdFailed if the rule requires SourceKit. + // This avoids triggering SourceKit initialization for SourceKit-free rules. + if requiresSourceKit && file.sourcekitdFailed { warnSourceKitFailedOnce() return false } diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index ec6a4f9000..3b9d9005dc 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -36,7 +36,7 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { // MARK: - CustomRules -struct CustomRules: Rule, CacheDescriptionProvider { +struct CustomRules: Rule, CacheDescriptionProvider, ConditionallySourceKitFree { var cacheDescription: String { configuration.cacheDescription } @@ -56,6 +56,11 @@ struct CustomRules: Rule, CacheDescriptionProvider { var configuration = CustomRulesConfiguration() + var isEffectivelySourceKitFree: Bool { + // Just a stub, will be implemented in a follow-up PR + false + } + func validate(file: SwiftLintFile) -> [StyleViolation] { var configurations = configuration.customRuleConfigurations diff --git a/Source/swiftlint/Commands/Rules.swift b/Source/swiftlint/Commands/Rules.swift index 9184305a15..f15b464665 100644 --- a/Source/swiftlint/Commands/Rules.swift +++ b/Source/swiftlint/Commands/Rules.swift @@ -132,7 +132,7 @@ private extension TextTable { configuredRule != nil ? "yes" : "no", ruleType.description.kind.rawValue, (rule is any AnalyzerRule) ? "yes" : "no", - (rule is any SourceKitFreeRule) ? "no" : "yes", + rule.requiresSourceKit ? "yes" : "no", truncate((defaultConfig ? rule : configuredRule ?? rule).createConfigurationDescription().oneLiner()), ]) } diff --git a/Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift b/Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift new file mode 100644 index 0000000000..1d69eb140c --- /dev/null +++ b/Tests/FrameworkTests/ConditionallySourceKitFreeTests.swift @@ -0,0 +1,87 @@ +@testable import SwiftLintCore +@testable import SwiftLintFramework +import XCTest + +final class ConditionallySourceKitFreeTests: XCTestCase { + // Mock rule for testing ConditionallySourceKitFree protocol + private struct MockConditionalRule: Rule, ConditionallySourceKitFree { + static let description = RuleDescription( + identifier: "mock_conditional", + name: "Mock Conditional Rule", + description: "A mock rule for testing ConditionallySourceKitFree", + kind: .style + ) + + var configuration = SeverityConfiguration(.warning) + var isEffectivelySourceKitFree = true + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } + } + + private struct MockSourceKitFreeRule: Rule, SourceKitFreeRule { + static let description = RuleDescription( + identifier: "mock_sourcekit_free", + name: "Mock SourceKit Free Rule", + description: "A mock rule that is always SourceKit-free", + kind: .style + ) + + var configuration = SeverityConfiguration(.warning) + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } + } + + private struct MockRegularRule: Rule { + static let description = RuleDescription( + identifier: "mock_regular", + name: "Mock Regular Rule", + description: "A mock rule that requires SourceKit", + kind: .style + ) + + var configuration = SeverityConfiguration(.warning) + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } + } + + func testRequiresSourceKitForDifferentRuleTypes() { + // SourceKitFreeRule should not require SourceKit + let sourceKitFreeRule = MockSourceKitFreeRule() + XCTAssertFalse(sourceKitFreeRule.requiresSourceKit) + + // ConditionallySourceKitFree rule that is effectively SourceKit-free + var conditionalRuleFree = MockConditionalRule() + conditionalRuleFree.isEffectivelySourceKitFree = true + XCTAssertFalse(conditionalRuleFree.requiresSourceKit) + + // ConditionallySourceKitFree rule that requires SourceKit + var conditionalRuleRequires = MockConditionalRule() + conditionalRuleRequires.isEffectivelySourceKitFree = false + XCTAssertTrue(conditionalRuleRequires.requiresSourceKit) + + // Regular rule should require SourceKit + let regularRule = MockRegularRule() + XCTAssertTrue(regularRule.requiresSourceKit) + } + + func testTypeCheckingBehavior() { + // Verify instance-level checks work correctly + let sourceKitFreeRule: any Rule = MockSourceKitFreeRule() + XCTAssertTrue(sourceKitFreeRule is any SourceKitFreeRule) + XCTAssertFalse(sourceKitFreeRule is any ConditionallySourceKitFree) + + let conditionalRule: any Rule = MockConditionalRule() + XCTAssertFalse(conditionalRule is any SourceKitFreeRule) + XCTAssertTrue(conditionalRule is any ConditionallySourceKitFree) + + let regularRule: any Rule = MockRegularRule() + XCTAssertFalse(regularRule is any SourceKitFreeRule) + XCTAssertFalse(regularRule is any ConditionallySourceKitFree) + } +} From c22de52b9be021dc92165f94b1e885063d505cd5 Mon Sep 17 00:00:00 2001 From: Kent Kaseda Date: Sun, 22 Jun 2025 08:32:48 +0900 Subject: [PATCH 39/80] Respect `ignore_swiftui_view_bodies` option in view builders and preview macros/providers (#6075) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Mösch --- CHANGELOG.md | 6 + .../Style/RedundantDiscardableLetRule.swift | 139 +++++++++++++++++- 2 files changed, 139 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35660e3208..54c8ad3589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ [JP Simard](https://github.com/jpsim) [Matt Pennig](https://github.com/pennig) +* Fix false positives of `redundant_discardable_let` rule in `@ViewBuilder` functions, + `#Preview` macro bodies and preview providers when `ignore_swiftui_view_bodies` is + enabled. + [kaseken](https://github.com/kaseken) + [#6063](https://github.com/realm/SwiftLint/issues/6063) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift index d7d6f2a1f6..c2fd3b0554 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/RedundantDiscardableLetRule.swift @@ -22,6 +22,25 @@ struct RedundantDiscardableLetRule: Rule { return Text("Hello, World!") } """, configuration: ["ignore_swiftui_view_bodies": true]), + Example(""" + @ViewBuilder + func bar() -> some View { + let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]), + Example(""" + #Preview { + let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]), + Example(""" + static var previews: some View { + let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]), ], triggeringExamples: [ Example("↓let _ = foo()"), @@ -32,10 +51,74 @@ struct RedundantDiscardableLetRule: Rule { Text("Hello, World!") } """), + Example(""" + @ViewBuilder + func bar() -> some View { + ↓let _ = foo() + return Text("Hello, World!") + } + """), + Example(""" + #Preview { + ↓let _ = foo() + return Text("Hello, World!") + } + """), + Example(""" + static var previews: some View { + ↓let _ = foo() + Text("Hello, World!") + } + """), + Example(""" + var notBody: some View { + ↓let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true], excludeFromDocumentation: true), + Example(""" + var body: some NotView { + ↓let _ = foo() + Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true], excludeFromDocumentation: true), ], corrections: [ Example("↓let _ = foo()"): Example("_ = foo()"), Example("if _ = foo() { ↓let _ = bar() }"): Example("if _ = foo() { _ = bar() }"), + Example(""" + var body: some View { + ↓let _ = foo() + Text("Hello, World!") + } + """): Example(""" + var body: some View { + _ = foo() + Text("Hello, World!") + } + """), + Example(""" + #Preview { + ↓let _ = foo() + return Text("Hello, World!") + } + """): Example(""" + #Preview { + _ = foo() + return Text("Hello, World!") + } + """), + Example(""" + var body: some View { + let _ = foo() + return Text("Hello, World!") + } + """, configuration: ["ignore_swiftui_view_bodies": true]): Example(""" + var body: some View { + let _ = foo() + return Text("Hello, World!") + } + """), ] ) } @@ -50,7 +133,7 @@ private extension RedundantDiscardableLetRule { private var codeBlockScopes = Stack() override func visit(_ node: AccessorBlockSyntax) -> SyntaxVisitorContinueKind { - codeBlockScopes.push(node.isViewBody ? .view : .normal) + codeBlockScopes.push(node.isViewBody || node.isPreviewProviderBody ? .view : .normal) return .visitChildren } @@ -58,8 +141,8 @@ private extension RedundantDiscardableLetRule { codeBlockScopes.pop() } - override func visit(_: CodeBlockSyntax) -> SyntaxVisitorContinueKind { - codeBlockScopes.push(.normal) + override func visit(_ node: CodeBlockSyntax) -> SyntaxVisitorContinueKind { + codeBlockScopes.push(node.isViewBuilderFunctionBody ? .view : .normal) return .visitChildren } @@ -67,6 +150,15 @@ private extension RedundantDiscardableLetRule { codeBlockScopes.pop() } + override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind { + codeBlockScopes.push(node.isPreviewMacroBody ? .view : .normal) + return .visitChildren + } + + override func visitPost(_: ClosureExprSyntax) { + codeBlockScopes.pop() + } + override func visitPost(_ node: VariableDeclSyntax) { if codeBlockScopes.peek() != .view || !configuration.ignoreSwiftUIViewBodies, node.bindingSpecifier.tokenKind == .keyword(.let), @@ -94,10 +186,45 @@ private extension AccessorBlockSyntax { if let binding = parent?.as(PatternBindingSyntax.self), binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "body", let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) { - return type.someOrAnySpecifier.text == "some" - && type.constraint.as(IdentifierTypeSyntax.self)?.name.text == "View" - && binding.parent?.parent?.is(VariableDeclSyntax.self) == true + return type.isView && binding.parent?.parent?.is(VariableDeclSyntax.self) == true } return false } + + var isPreviewProviderBody: Bool { + guard let binding = parent?.as(PatternBindingSyntax.self), + binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == "previews", + let bindingList = binding.parent?.as(PatternBindingListSyntax.self), + let variableDecl = bindingList.parent?.as(VariableDeclSyntax.self), + variableDecl.modifiers.contains(keyword: .static), + variableDecl.bindingSpecifier.tokenKind == .keyword(.var), + let type = binding.typeAnnotation?.type.as(SomeOrAnyTypeSyntax.self) else { + return false + } + + return type.isView + } +} + +private extension CodeBlockSyntax { + var isViewBuilderFunctionBody: Bool { + guard let functionDecl = parent?.as(FunctionDeclSyntax.self), + functionDecl.attributes.contains(attributeNamed: "ViewBuilder") else { + return false + } + return functionDecl.signature.returnClause?.type.as(SomeOrAnyTypeSyntax.self)?.isView ?? false + } +} + +private extension ClosureExprSyntax { + var isPreviewMacroBody: Bool { + parent?.as(MacroExpansionExprSyntax.self)?.macroName.text == "Preview" + } +} + +private extension SomeOrAnyTypeSyntax { + var isView: Bool { + someOrAnySpecifier.text == "some" && + constraint.as(IdentifierTypeSyntax.self)?.name.text == "View" + } } From f6c96330877caaa6e0a4e1ee733bbf45373ac2f5 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Sun, 22 Jun 2025 10:19:14 -0400 Subject: [PATCH 40/80] Add `RegexConfiguration.ExecutionMode` (#6128) * Add `RegexConfiguration.ExecutionMode` To help migrate custom rules to SwiftSyntax. Not wired up yet, just the configuration parsing and defaults. Will wire it up in the next PR. The diff looks big, but it's 500+ lines of tests, with ~45 lines of actually new code. * Docs * Address PR feedback - Add `default` case to ExecutionMode enum instead of using optional - Change configuration key from `mode` to `execution_mode` for consistency - Move default execution mode logic to runtime instead of configuration time - Refactor test functions to use throws instead of do-catch --- .../RegexConfiguration.swift | 21 + .../Rules/CustomRules.swift | 30 +- Tests/FrameworkTests/CustomRulesTests.swift | 499 ++++++++++++++++++ 3 files changed, 545 insertions(+), 5 deletions(-) diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index 6329261b45..dbb7d626a6 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -4,6 +4,15 @@ import SourceKittenFramework /// A rule configuration used for defining custom rules in yaml. public struct RegexConfiguration: SeverityBasedRuleConfiguration, Hashable, CacheDescriptionProvider, InlinableOptionType { + /// The execution mode for this custom rule. + public enum ExecutionMode: String, Codable, Sendable { + /// Uses SwiftSyntax to obtain syntax token kinds. + case swiftsyntax + /// Uses SourceKit to obtain syntax token kinds. + case sourcekit + /// Uses SwiftSyntax by default unless overridden to use SourceKit. + case `default` + } /// The identifier for this custom rule. public let identifier: String /// The name for this custom rule. @@ -24,6 +33,8 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, public var severityConfiguration = SeverityConfiguration(.warning) /// The index of the regex capture group to match. public var captureGroup = 0 + /// The execution mode for this rule. + public var executionMode: ExecutionMode = .default public var cacheDescription: String { let jsonObject: [String] = [ @@ -36,6 +47,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, SyntaxKind.allKinds.subtracting(excludedMatchKinds) .map(\.rawValue).sorted(by: <).joined(separator: ","), severity.rawValue, + executionMode.rawValue, ] if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject), let jsonString = String(data: jsonData, encoding: .utf8) { @@ -57,6 +69,7 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, self.identifier = identifier } + // swiftlint:disable:next cyclomatic_complexity public mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any], let regexString = configurationDict[$regex.key] as? String else { @@ -97,11 +110,19 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, self.captureGroup = captureGroup } + if let modeString = configurationDict["execution_mode"] as? String { + guard let mode = ExecutionMode(rawValue: modeString) else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + self.executionMode = mode + } + self.excludedMatchKinds = try self.excludedMatchKinds(from: configurationDict) } public func hash(into hasher: inout Hasher) { hasher.combine(identifier) + hasher.combine(executionMode) } package func shouldValidate(filePath: String) -> Bool { diff --git a/Source/SwiftLintFramework/Rules/CustomRules.swift b/Source/SwiftLintFramework/Rules/CustomRules.swift index 3b9d9005dc..d54c57488e 100644 --- a/Source/SwiftLintFramework/Rules/CustomRules.swift +++ b/Source/SwiftLintFramework/Rules/CustomRules.swift @@ -7,19 +7,33 @@ struct CustomRulesConfiguration: RuleConfiguration, CacheDescriptionProvider { var parameterDescription: RuleConfigurationDescription? { RuleConfigurationOption.noOptions } var cacheDescription: String { - customRuleConfigurations + let configsDescription = customRuleConfigurations .sorted { $0.identifier < $1.identifier } .map(\.cacheDescription) .joined(separator: "\n") + + if let defaultMode = defaultExecutionMode { + return "default_execution_mode:\(defaultMode.rawValue)\n\(configsDescription)" + } + return configsDescription } var customRuleConfigurations = [RegexConfiguration]() + var defaultExecutionMode: RegexConfiguration.ExecutionMode? mutating func apply(configuration: Any) throws { guard let configurationDict = configuration as? [String: Any] else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } - for (key, value) in configurationDict { + // Parse default execution mode if present + if let defaultModeString = configurationDict["default_execution_mode"] as? String { + guard let mode = RegexConfiguration.ExecutionMode(rawValue: defaultModeString) else { + throw Issue.invalidConfiguration(ruleID: Parent.identifier) + } + defaultExecutionMode = mode + } + + for (key, value) in configurationDict where key != "default_execution_mode" { var ruleConfiguration = RegexConfiguration(identifier: key) do { @@ -50,15 +64,21 @@ struct CustomRules: Rule, CacheDescriptionProvider, ConditionallySourceKitFree { name: "Custom Rules", description: """ Create custom rules by providing a regex string. Optionally specify what syntax kinds to match against, \ - the severity level, and what message to display. + the severity level, and what message to display. Rules default to SwiftSyntax mode for improved \ + performance. Use `execution_mode: sourcekit` or `default_execution_mode: sourcekit` for SourceKit mode. """, kind: .style) var configuration = CustomRulesConfiguration() + /// Returns true if all configured custom rules use SwiftSyntax mode, making this rule effectively SourceKit-free. var isEffectivelySourceKitFree: Bool { - // Just a stub, will be implemented in a follow-up PR - false + configuration.customRuleConfigurations.allSatisfy { config in + let effectiveMode = config.executionMode == .default + ? (configuration.defaultExecutionMode ?? .swiftsyntax) + : config.executionMode + return effectiveMode == .swiftsyntax + } } func validate(file: SwiftLintFile) -> [StyleViolation] { diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index efbfa43f5a..98b67a083c 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -33,6 +33,7 @@ final class CustomRulesTests: SwiftLintTestCase { comp.regex = "regex" comp.severityConfiguration = SeverityConfiguration(.error) comp.excludedMatchKinds = SyntaxKind.allKinds.subtracting([.comment]) + comp.executionMode = .default var compRules = CustomRulesConfiguration() compRules.customRuleConfigurations = [comp] do { @@ -60,6 +61,7 @@ final class CustomRulesTests: SwiftLintTestCase { comp.regex = "regex" comp.severityConfiguration = SeverityConfiguration(.error) comp.excludedMatchKinds = Set([.comment]) + comp.executionMode = .default var compRules = CustomRulesConfiguration() compRules.customRuleConfigurations = [comp] do { @@ -512,6 +514,367 @@ final class CustomRulesTests: SwiftLintTestCase { XCTAssertTrue(violations[3].isSuperfluousDisableCommandViolation(for: "rule2")) } + // MARK: - ExecutionMode Tests (Phase 1) + + func testRegexConfigurationParsesExecutionMode() throws { + let configDict = [ + "regex": "pattern", + "execution_mode": "swiftsyntax", + ] + + var regexConfig = Configuration(identifier: "test_rule") + try regexConfig.apply(configuration: configDict) + XCTAssertEqual(regexConfig.executionMode, .swiftsyntax) + } + + func testRegexConfigurationParsesSourceKitMode() throws { + let configDict = [ + "regex": "pattern", + "execution_mode": "sourcekit", + ] + + var regexConfig = Configuration(identifier: "test_rule") + try regexConfig.apply(configuration: configDict) + XCTAssertEqual(regexConfig.executionMode, .sourcekit) + } + + func testRegexConfigurationWithoutModeIsDefault() throws { + let configDict = [ + "regex": "pattern", + ] + + var regexConfig = Configuration(identifier: "test_rule") + try regexConfig.apply(configuration: configDict) + XCTAssertEqual(regexConfig.executionMode, .default) + } + + func testRegexConfigurationRejectsInvalidMode() { + let configDict = [ + "regex": "pattern", + "execution_mode": "invalid_mode", + ] + + var regexConfig = Configuration(identifier: "test_rule") + checkError(Issue.invalidConfiguration(ruleID: CustomRules.identifier)) { + try regexConfig.apply(configuration: configDict) + } + } + + func testCustomRulesConfigurationParsesDefaultExecutionMode() throws { + let configDict: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "my_rule": [ + "regex": "pattern", + ], + ] + + var customRulesConfig = CustomRulesConfiguration() + try customRulesConfig.apply(configuration: configDict) + XCTAssertEqual(customRulesConfig.defaultExecutionMode, .swiftsyntax) + XCTAssertEqual(customRulesConfig.customRuleConfigurations.count, 1) + XCTAssertEqual(customRulesConfig.customRuleConfigurations[0].executionMode, .default) + } + + func testCustomRulesAppliesDefaultModeToRulesWithoutExplicitMode() throws { + let configDict: [String: Any] = [ + "default_execution_mode": "sourcekit", + "rule1": [ + "regex": "pattern1", + ], + "rule2": [ + "regex": "pattern2", + "execution_mode": "swiftsyntax", + ], + ] + + var customRulesConfig = CustomRulesConfiguration() + try customRulesConfig.apply(configuration: configDict) + XCTAssertEqual(customRulesConfig.defaultExecutionMode, .sourcekit) + XCTAssertEqual(customRulesConfig.customRuleConfigurations.count, 2) + + // rule1 should have default mode + let rule1 = customRulesConfig.customRuleConfigurations.first { $0.identifier == "rule1" } + XCTAssertEqual(rule1?.executionMode, .default) + + // rule2 should keep its explicit mode + let rule2 = customRulesConfig.customRuleConfigurations.first { $0.identifier == "rule2" } + XCTAssertEqual(rule2?.executionMode, .swiftsyntax) + } + + func testCustomRulesConfigurationRejectsInvalidDefaultMode() { + let configDict: [String: Any] = [ + "default_execution_mode": "invalid", + "my_rule": [ + "regex": "pattern", + ], + ] + + var customRulesConfig = CustomRulesConfiguration() + checkError(Issue.invalidConfiguration(ruleID: CustomRules.identifier)) { + try customRulesConfig.apply(configuration: configDict) + } + } + + func testExecutionModeIncludedInCacheDescription() { + var regexConfig = Configuration(identifier: "test_rule") + regexConfig.regex = "pattern" + regexConfig.executionMode = .swiftsyntax + + XCTAssertTrue(regexConfig.cacheDescription.contains("swiftsyntax")) + } + + func testExecutionModeAffectsHash() { + var config1 = Configuration(identifier: "test_rule") + config1.regex = "pattern" + config1.executionMode = .swiftsyntax + + var config2 = Configuration(identifier: "test_rule") + config2.regex = "pattern" + config2.executionMode = .sourcekit + + var config3 = Configuration(identifier: "test_rule") + config3.regex = "pattern" + config3.executionMode = .default + + // Different execution modes should produce different hashes + XCTAssertNotEqual(config1.hashValue, config2.hashValue) + XCTAssertNotEqual(config1.hashValue, config3.hashValue) + XCTAssertNotEqual(config2.hashValue, config3.hashValue) + } + + // MARK: - Phase 2 Tests: SwiftSyntax Mode Execution + + func testCustomRuleUsesSwiftSyntaxModeWhenConfigured() throws { + // Test that a rule configured with swiftsyntax mode works correctly + let customRules: [String: Any] = [ + "no_foo": [ + "regex": "\\bfoo\\b", + "execution_mode": "swiftsyntax", + "message": "Don't use foo", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].ruleIdentifier, "no_foo") + XCTAssertEqual(violations[0].reason, "Don't use foo") + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 5) + } + + func testCustomRuleWithoutMatchKindsUsesSwiftSyntaxByDefault() throws { + // When default_execution_mode is swiftsyntax, rules without match_kinds should use it + let customRules: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "no_bar": [ + "regex": "\\bbar\\b", + "message": "Don't use bar", + ], + ] + + let example = Example("let bar = 42 // bar is not allowed") + let violations = try violations(forExample: example, customRules: customRules) + + // Should find both occurrences of 'bar' since no match_kinds filtering + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 5) + XCTAssertEqual(violations[1].location.line, 1) + XCTAssertEqual(violations[1].location.character, 18) + } + + func testCustomRuleDefaultsToSwiftSyntaxWhenNoModeSpecified() throws { + // When NO execution mode is specified (neither default nor per-rule), it should default to swiftsyntax + let customRules: [String: Any] = [ + "no_foo": [ + "regex": "\\bfoo\\b", + "message": "Don't use foo", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should work correctly with implicit swiftsyntax mode + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].ruleIdentifier, "no_foo") + XCTAssertEqual(violations[0].reason, "Don't use foo") + + // Verify the rule is effectively SourceKit-free + let configuration = try SwiftLintFramework.Configuration(dict: [ + "only_rules": ["custom_rules"], + "custom_rules": customRules, + ]) + + guard let customRule = configuration.rules.first(where: { $0 is CustomRules }) as? CustomRules else { + XCTFail("Expected CustomRules in configuration") + return + } + + XCTAssertTrue(customRule.isEffectivelySourceKitFree, + "Rule should be effectively SourceKit-free when defaulting to swiftsyntax") + } + + func testCustomRuleWithMatchKindsUsesSwiftSyntaxWhenConfigured() throws { + // Phase 4: Rules with match_kinds in swiftsyntax mode should use SwiftSyntax bridging + let customRules: [String: Any] = [ + "comment_foo": [ + "regex": "foo", + "execution_mode": "swiftsyntax", + "match_kinds": "comment", + "message": "No foo in comments", + ], + ] + + let example = Example(""" + let foo = 42 // This foo should match + let bar = 42 // This should not match + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should only match 'foo' in comment, not in code + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 23) // Position of 'foo' in comment + } + + func testCustomRuleWithKindFilteringDefaultsToSwiftSyntax() throws { + // When using kind filtering without specifying mode, it should default to swiftsyntax + let customRules: [String: Any] = [ + "no_keywords": [ + "regex": "\\b\\w+\\b", + "excluded_match_kinds": "keyword", + "message": "Found non-keyword", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'foo' and '42' but not 'let' (keyword) + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].location.character, 5) // 'foo' + XCTAssertEqual(violations[1].location.character, 11) // '42' + + // Verify the rule is effectively SourceKit-free + let configuration = try SwiftLintFramework.Configuration(dict: [ + "only_rules": ["custom_rules"], + "custom_rules": customRules, + ]) + + guard let customRule = configuration.rules.first(where: { $0 is CustomRules }) as? CustomRules else { + XCTFail("Expected CustomRules in configuration") + return + } + + XCTAssertTrue(customRule.isEffectivelySourceKitFree, + "Rule with kind filtering should default to swiftsyntax mode") + } + + func testCustomRuleWithExcludedMatchKindsUsesSwiftSyntaxWithDefaultMode() throws { + // Phase 4: Rules with excluded_match_kinds should use SwiftSyntax when default mode is swiftsyntax + let customRules: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "no_foo_outside_comments": [ + "regex": "foo", + "excluded_match_kinds": "comment", + "message": "No foo outside comments", + ], + ] + + let example = Example(""" + let foo = 42 // This foo in comment should not match + let foobar = 42 + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'foo' in code but not in comment + XCTAssertEqual(violations.count, 2) + XCTAssertEqual(violations[0].location.line, 1) + XCTAssertEqual(violations[0].location.character, 5) // 'foo' in variable name + XCTAssertEqual(violations[1].location.line, 2) + XCTAssertEqual(violations[1].location.character, 5) // 'foo' in foobar + } + + func testSwiftSyntaxModeProducesSameResultsAsSourceKitForSimpleRules() throws { + // Test that both modes produce identical results for rules without kind filtering + let pattern = "\\bTODO\\b" + let message = "TODOs should be resolved" + + let swiftSyntaxRules: [String: Any] = [ + "todo_rule": [ + "regex": pattern, + "execution_mode": "swiftsyntax", + "message": message, + ], + ] + + let sourceKitRules: [String: Any] = [ + "todo_rule": [ + "regex": pattern, + "execution_mode": "sourcekit", + "message": message, + ], + ] + + let example = Example(""" + // TODO: Fix this later + func doSomething() { + // Another TODO item + print("TODO is not matched in strings") + } + """) + + let swiftSyntaxViolations = try violations(forExample: example, customRules: swiftSyntaxRules) + let sourceKitViolations = try violations(forExample: example, customRules: sourceKitRules) + + // Both modes should produce identical results + XCTAssertEqual(swiftSyntaxViolations.count, sourceKitViolations.count) + XCTAssertEqual(swiftSyntaxViolations.count, 3) // Two in comments, one in string + + // Verify locations match + for (ssViolation, skViolation) in zip(swiftSyntaxViolations, sourceKitViolations) { + XCTAssertEqual(ssViolation.location.line, skViolation.location.line) + XCTAssertEqual(ssViolation.location.character, skViolation.location.character) + XCTAssertEqual(ssViolation.reason, skViolation.reason) + } + } + + func testSwiftSyntaxModeWithCaptureGroups() throws { + // Test that capture groups work correctly in SwiftSyntax mode + let customRules: [String: Any] = [ + "number_suffix": [ + "regex": "\\b(\\d+)_suffix\\b", + "capture_group": 1, + "execution_mode": "swiftsyntax", + "message": "Number found", + ], + ] + + let example = Example("let value = 42_suffix + 100_suffix") + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 2) + // First capture group should highlight just the number part + XCTAssertEqual(violations[0].location.character, 13) // Position of "42" + XCTAssertEqual(violations[1].location.character, 25) // Position of "100" + } + + func testSwiftSyntaxModeRespectsIncludedExcludedPaths() throws { + // Verify that included/excluded path filtering works in SwiftSyntax mode + var regexConfig = Configuration(identifier: "test_rule") + regexConfig.regex = "pattern" + regexConfig.executionMode = .swiftsyntax + regexConfig.included = [try RegularExpression(pattern: "\\.swift$")] + regexConfig.excluded = [try RegularExpression(pattern: "Tests")] + + XCTAssertTrue(regexConfig.shouldValidate(filePath: "/path/to/file.swift")) + XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/file.m")) + XCTAssertFalse(regexConfig.shouldValidate(filePath: "/path/to/Tests/file.swift")) + } + // MARK: - Private private func getCustomRules(_ extraConfig: [String: Any] = [:]) -> (Configuration, CustomRules) { @@ -574,6 +937,142 @@ final class CustomRulesTests: SwiftLintTestCase { customRules.configuration = customRuleConfiguration return customRules } + + // MARK: - Phase 4 Tests: SwiftSyntax Mode WITH Kind Filtering + + func testSwiftSyntaxModeWithMatchKindsProducesCorrectResults() throws { + // Test various syntax kinds with SwiftSyntax bridging + let customRules: [String: Any] = [ + "keyword_test": [ + "regex": "\\b\\w+\\b", + "execution_mode": "swiftsyntax", + "match_kinds": "keyword", + "message": "Found keyword", + ], + ] + + let example = Example(""" + let value = 42 + func test() { + return value + } + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'let', 'func', and 'return' keywords + XCTAssertEqual(violations.count, 3) + + // Verify the locations correspond to keywords + let expectedLocations = [ + (line: 1, character: 1), // 'let' + (line: 2, character: 1), // 'func' + (line: 3, character: 5), // 'return' + ] + + for (index, expected) in expectedLocations.enumerated() { + XCTAssertEqual(violations[index].location.line, expected.line) + XCTAssertEqual(violations[index].location.character, expected.character) + } + } + + func testSwiftSyntaxModeWithExcludedKindsFiltersCorrectly() throws { + // Test that excluded kinds are properly filtered out + let customRules: [String: Any] = [ + "no_identifier": [ + "regex": "\\b\\w+\\b", + "execution_mode": "swiftsyntax", + "excluded_match_kinds": ["identifier", "typeidentifier"], + "message": "Found non-identifier", + ], + ] + + let example = Example(""" + let value: Int = 42 + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match 'let' (keyword) and '42' (number), but not 'value' or 'Int' + XCTAssertEqual(violations.count, 2) + } + + func testSwiftSyntaxModeHandlesComplexKindMatching() throws { + // Test matching multiple specific kinds + let customRules: [String: Any] = [ + "special_tokens": [ + "regex": "\\S+", + "execution_mode": "swiftsyntax", + "match_kinds": ["string", "number", "comment"], + "message": "Found special token", + ], + ] + + let example = Example(""" + let name = "Alice" // User name + let age = 25 + """) + let violations = try violations(forExample: example, customRules: customRules) + + // Should match "Alice" (string), 25 (number), and "// User name" (comment) + // The regex \S+ will match non-whitespace sequences + XCTAssertGreaterThanOrEqual(violations.count, 3) + } + + func testSwiftSyntaxModeWorksWithCaptureGroups() throws { + // Test that capture groups work correctly with SwiftSyntax mode + let customRules: [String: Any] = [ + "string_content": [ + "regex": #""([^"]+)""#, + "execution_mode": "swiftsyntax", + "match_kinds": "string", + "capture_group": 1, + "message": "String content", + ], + ] + + let example = Example(#"let greeting = "Hello, World!""#) + let violations = try violations(forExample: example, customRules: customRules) + + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].location.character, 17) // Start of "Hello, World!" content + } + + func testSwiftSyntaxModeRespectsSourceKitModeOverride() throws { + // Test that explicit sourcekit mode overrides default swiftsyntax mode + let customRules: [String: Any] = [ + "default_execution_mode": "swiftsyntax", + "sourcekit_rule": [ + "regex": "foo", + "execution_mode": "sourcekit", + "match_kinds": "identifier", + "message": "Found foo", + ], + ] + + let example = Example("let foo = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should still work correctly with explicit sourcekit mode + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations[0].location.character, 5) + } + + func testSwiftSyntaxModeHandlesEmptyBridging() throws { + // Test graceful handling when no tokens match the specified kinds + let customRules: [String: Any] = [ + "attribute_only": [ + "regex": "\\w+", + "execution_mode": "swiftsyntax", + "match_kinds": "attributeBuiltin", // Very specific kind that won't match normal code + "message": "Found attribute", + ], + ] + + let example = Example("let value = 42") + let violations = try violations(forExample: example, customRules: customRules) + + // Should produce no violations since there are no built-in attributes + XCTAssertEqual(violations.count, 0) + } } private extension StyleViolation { From ac476aaf137609b75e677db38acd1be7b014ccfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 22 Jun 2025 20:24:37 +0200 Subject: [PATCH 41/80] Run Swift linting job with Bazel for better caching (#6130) --- .github/workflows/lint.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3730369b46..80dcff14a9 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,11 +13,20 @@ jobs: lint-swift: name: Lint Swift runs-on: ubuntu-24.04 # "Noble Numbat" - container: swift:6.1-noble steps: - uses: actions/checkout@v4 + - name: Create ci.bazelrc file + run: echo "$CI_BAZELRC_FILE_CONTENT" | base64 -d > ci.bazelrc + env: + CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} - name: Lint - run: swift run swiftlint --reporter github-actions-logging --strict 2> /dev/null + run: | + export PATH="/usr/share/swift/usr/bin:$PATH" + git apply --ignore-whitespace .bcr/patches/no-warnings-as-errors.patch + bazel build --config release //:swiftlint + ./bazel-bin/swiftlint lint --reporter github-actions-logging --strict 2> /dev/null + env: + CC: clang lint-markdown: name: Lint Markdown runs-on: ubuntu-24.04 From 388d45246e89835280b8752d23101fe2a096b6d0 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Mon, 23 Jun 2025 07:43:04 -0400 Subject: [PATCH 42/80] Migrate ExpiringTodoRule from SourceKit to SwiftSyntax (#6113) --- CHANGELOG.md | 1 + .../Rules/Lint/ExpiringTodoRule.swift | 210 ++++++++++++------ 2 files changed, 148 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54c8ad3589..3d5f32c0f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ * `accessibility_label_for_image` * `accessibility_trait_for_button` * `closure_end_indentation` + * `expiring_todo` * `file_length` * `line_length` * `vertical_whitespace` diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift index c64c93c681..49cf8c43d3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ExpiringTodoRule.swift @@ -1,7 +1,8 @@ import Foundation -import SourceKittenFramework +import SwiftSyntax -struct ExpiringTodoRule: OptInRule { +@SwiftSyntaxRule(optIn: true) +struct ExpiringTodoRule: Rule { enum ExpiryViolationLevel { case approachingExpiry case expired @@ -10,11 +11,11 @@ struct ExpiringTodoRule: OptInRule { var reason: String { switch self { case .approachingExpiry: - return "TODO/FIXME is approaching its expiry and should be resolved soon" + "TODO/FIXME is approaching its expiry and should be resolved soon" case .expired: - return "TODO/FIXME has expired and must be resolved" + "TODO/FIXME has expired and must be resolved" case .badFormatting: - return "Expiring TODO/FIXME is incorrectly formatted" + "Expiring TODO/FIXME is incorrectly formatted" } } } @@ -45,73 +46,142 @@ struct ExpiringTodoRule: OptInRule { ) var configuration = ExpiringTodoConfiguration() +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - let regex = #""" - \b(?:TODO|FIXME)(?::|\b)(?:(?!\b(?:TODO|FIXME)(?::|\b)).)*?\# - \\#(configuration.dateDelimiters.opening)\# - (\d{1,4}\\#(configuration.dateSeparator)\d{1,4}\\#(configuration.dateSeparator)\d{1,4})\# - \\#(configuration.dateDelimiters.closing) - """# - - return file.matchesAndSyntaxKinds(matching: regex).compactMap { checkingResult, syntaxKinds in - guard - syntaxKinds.allSatisfy(\.isCommentLike), - checkingResult.numberOfRanges > 1, - case let range = checkingResult.range(at: 1), - let violationLevel = violationLevel(for: expiryDate(file: file, range: range)), - let severity = severity(for: violationLevel) else { - return nil +private extension ExpiringTodoRule { + final class Visitor: ViolationsSyntaxVisitor { + private lazy var regex: NSRegularExpression = { + let pattern = #""" + \b(?:TODO|FIXME)(?::|\b)(?:(?!\b(?:TODO|FIXME)(?::|\b)).)*?\# + \\#(configuration.dateDelimiters.opening)\# + (\d{1,4}\\#(configuration.dateSeparator)\d{1,4}\\#(configuration.dateSeparator)\d{1,4})\# + \\#(configuration.dateDelimiters.closing) + """# + return SwiftLintCore.regex(pattern) + }() + + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + // Process each comment individually + for token in node.tokens(viewMode: .sourceAccurate) { + processTrivia( + token.leadingTrivia, + baseOffset: token.position.utf8Offset + ) + processTrivia( + token.trailingTrivia, + baseOffset: token.endPositionBeforeTrailingTrivia.utf8Offset + ) } - return StyleViolation( - ruleDescription: Self.description, - severity: severity, - location: Location(file: file, characterOffset: range.location), - reason: violationLevel.reason - ) + return .skipChildren } - } - private func expiryDate(file: SwiftLintFile, range: NSRange) -> Date? { - let expiryDateString = file.contents.bridge() - .substring(with: range) - .trimmingCharacters(in: .whitespacesAndNewlines) + private func processTrivia(_ trivia: Trivia, baseOffset: Int) { + var triviaOffset = baseOffset - let formatter = DateFormatter() - formatter.calendar = .current - formatter.dateFormat = configuration.dateFormat + for (index, piece) in trivia.enumerated() { + defer { triviaOffset += piece.sourceLength.utf8Length } - return formatter.date(from: expiryDateString) - } + guard let commentText = piece.commentText else { continue } + + // Handle multiline comments by checking consecutive line comments + if piece.isLineComment { + var combinedText = commentText + let currentOffset = triviaOffset + + // Look ahead for consecutive line comments + let remainingTrivia = trivia.dropFirst(index + 1) - private func severity(for violationLevel: ExpiryViolationLevel) -> ViolationSeverity? { - switch violationLevel { - case .approachingExpiry: - return configuration.approachingExpirySeverity.severity - case .expired: - return configuration.expiredSeverity.severity - case .badFormatting: - return configuration.badFormattingSeverity.severity + for nextPiece in remainingTrivia { + if case .lineComment(let nextText) = nextPiece { + // Check if it's a continuation (starts with //) + if nextText.hasPrefix("//") { + combinedText += "\n" + nextText + } else { + break + } + } else if !nextPiece.isWhitespace { + break + } + } + + processComment(combinedText, offset: currentOffset) + } else { + processComment(commentText, offset: triviaOffset) + } + } + } + + private func processComment(_ commentText: String, offset: Int) { + let matches = regex.matches(in: commentText, options: [], range: commentText.fullNSRange) + let nsStringComment = commentText.bridge() + + for match in matches { + guard match.numberOfRanges > 1 else { continue } + + // Get the date capture group (second capture group, index 1) + let dateRange = match.range(at: 1) + guard dateRange.location != NSNotFound else { continue } + + let matchOffset = offset + dateRange.location + let matchPosition = AbsolutePosition(utf8Offset: matchOffset) + + let dateString = nsStringComment.substring(with: dateRange) + .trimmingCharacters(in: .whitespacesAndNewlines) + + if let violationLevel = getViolationLevel(for: parseDate(dateString: dateString)), + let severity = getSeverity(for: violationLevel) { + let violation = ReasonedRuleViolation( + position: matchPosition, + reason: violationLevel.reason, + severity: severity + ) + violations.append(violation) + } + } } - } - private func violationLevel(for expiryDate: Date?) -> ExpiryViolationLevel? { - guard let expiryDate else { - return .badFormatting + private func parseDate(dateString: String) -> Date? { + let formatter = DateFormatter() + formatter.calendar = .current + formatter.dateFormat = configuration.dateFormat + return formatter.date(from: dateString) } - guard expiryDate.isAfterToday else { - return .expired + + private func getSeverity(for violationLevel: ExpiryViolationLevel) -> ViolationSeverity? { + switch violationLevel { + case .approachingExpiry: + configuration.approachingExpirySeverity.severity + case .expired: + configuration.expiredSeverity.severity + case .badFormatting: + configuration.badFormattingSeverity.severity + } } - guard let approachingDate = Calendar.current.date( - byAdding: .day, - value: -configuration.approachingExpiryThreshold, - to: expiryDate) else { + + private func getViolationLevel(for expiryDate: Date?) -> ExpiryViolationLevel? { + guard let expiryDate else { + return .badFormatting + } + + guard expiryDate.isAfterToday else { + return .expired + } + + let approachingDate = Calendar.current.date( + byAdding: .day, + value: -configuration.approachingExpiryThreshold, + to: expiryDate + ) + + guard let approachingDate else { return nil + } + + return approachingDate.isAfterToday ? + nil : + .approachingExpiry } - return approachingDate.isAfterToday ? - nil : - .approachingExpiry } } @@ -121,9 +191,23 @@ private extension Date { } } -private extension SyntaxKind { - /// Returns if the syntax kind is comment-like. - var isCommentLike: Bool { - Self.commentKinds.contains(self) - } +private extension TriviaPiece { + var isLineComment: Bool { + switch self { + case .lineComment, .docLineComment: + true + default: + false + } + } + + var commentText: String? { + switch self { + case .lineComment(let text), .blockComment(let text), + .docLineComment(let text), .docBlockComment(let text): + text + default: + nil + } + } } From 4b9208a37b432104faa4ddc22d7b7306410e7b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Tue, 24 Jun 2025 09:47:40 +0200 Subject: [PATCH 43/80] Ignore various assignment operators in `void_function_in_ternary` (#6133) --- CHANGELOG.md | 5 +++ .../VoidFunctionInTernaryConditionRule.swift | 44 ++++++++++++++++--- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5f32c0f7..c5daf95dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ [imsonalbajaj](https://github.com/imsonalbajaj) [#6054](https://github.com/realm/SwiftLint/issues/6054) +* Ignore various assignment operators like `=`, `+=`, `&=`, etc. with right-hand side + ternary expressions otherwise violating the `void_function_in_ternary` rule. + [SimplyDanny](https://github.com/SimplyDanny) + [#5611](https://github.com/realm/SwiftLint/issues/5611) + * Rewrite the following rules with SwiftSyntax: * `accessibility_label_for_image` * `accessibility_trait_for_button` diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift index 8d6cc648c5..25f5dd1575 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift @@ -11,7 +11,6 @@ struct VoidFunctionInTernaryConditionRule: Rule { kind: .idiomatic, minSwiftVersion: .fiveDotOne, nonTriggeringExamples: [ - Example("let result = success ? foo() : bar()"), Example(""" if success { askQuestion() @@ -60,6 +59,14 @@ struct VoidFunctionInTernaryConditionRule: Rule { subscript(index: Int) -> Int { index == 0 ? defaultValue() : compute(index) """), + Example(""" + var a = b ? c() : d() + a += b ? c() : d() + a -= b ? c() : d() + a *= b ? c() : d() + a &<<= b ? c() : d() + a &-= b ? c() : d() + """), ], triggeringExamples: [ Example("success ↓? askQuestion() : exit()"), @@ -144,18 +151,43 @@ private extension VoidFunctionInTernaryConditionRule { private extension ExprListSyntax { var containsAssignment: Bool { - children(viewMode: .sourceAccurate).contains(where: { $0.is(AssignmentExprSyntax.self) }) + children(viewMode: .sourceAccurate).contains { + if let binOp = $0.as(BinaryOperatorExprSyntax.self) { + // https://developer.apple.com/documentation/swift/operator-declarations + return [ + "*=", + "/=", + "%=", + "+=", + "-=", + "<<=", + ">>=", + "&=", + "|=", + "^=", + "&*=", + "&+=", + "&-=", + "&<<=", + "&>>=", + ".&=", + ".|=", + ".^=", + ].contains(binOp.operator.text) + } + return $0.is(AssignmentExprSyntax.self) + } } } private extension CodeBlockItemSyntax { var isImplicitReturn: Bool { - isClosureImplictReturn || isFunctionImplicitReturn || + isClosureImplicitReturn || isFunctionImplicitReturn || isVariableImplicitReturn || isSubscriptImplicitReturn || - isAcessorImplicitReturn + isAccessorImplicitReturn } - var isClosureImplictReturn: Bool { + var isClosureImplicitReturn: Bool { guard let parent = parent?.as(CodeBlockItemListSyntax.self), let grandparent = parent.parent else { return false @@ -191,7 +223,7 @@ private extension CodeBlockItemSyntax { return parent.children(viewMode: .sourceAccurate).count == 1 && subscriptDecl.allowsImplicitReturns } - var isAcessorImplicitReturn: Bool { + var isAccessorImplicitReturn: Bool { guard let parent = parent?.as(CodeBlockItemListSyntax.self), parent.parent?.parent?.as(AccessorDeclSyntax.self) != nil else { return false From d14e22a57f16513507da188a2911bb3dc72bf5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Tue, 24 Jun 2025 10:07:17 +0200 Subject: [PATCH 44/80] Migrate Linux builds from Azure to GitHub Actions (#6132) This allows for better reusability and integration. macOS builds still run on Azure due to GitHub's limitation of up to 5 macOS jobs running concurrently. --- .azure/templates/run-make.yml | 16 ------- .github/actions/bazel-linux-build/action.yml | 20 ++++++++ .github/actions/run-make/action.yml | 19 ++++++++ .github/workflows/build.yml | 43 +++++++++++++++++ .github/workflows/lint.yml | 20 +++----- .github/workflows/test.yml | 29 ++++++++++++ .gitignore | 2 +- azure-pipelines.yml | 50 ++------------------ 8 files changed, 121 insertions(+), 78 deletions(-) delete mode 100644 .azure/templates/run-make.yml create mode 100644 .github/actions/bazel-linux-build/action.yml create mode 100644 .github/actions/run-make/action.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml diff --git a/.azure/templates/run-make.yml b/.azure/templates/run-make.yml deleted file mode 100644 index 8861ec66d8..0000000000 --- a/.azure/templates/run-make.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Run the commands in the Makefile for the specified rule. - -parameters: - - name: rule - type: string - -steps: - - script: >- - awk ' - $0 ~ "${{ parameters.rule }}:" { in_rule = 1; next } - in_rule && /^\t/ { print $0 } - in_rule && !/^\t/ { in_rule = 0 } - ' Makefile | while IFS= read -r command; do - eval "$command" - done - displayName: Run `${{ parameters.rule }}` rule diff --git a/.github/actions/bazel-linux-build/action.yml b/.github/actions/bazel-linux-build/action.yml new file mode 100644 index 0000000000..40293a7ced --- /dev/null +++ b/.github/actions/bazel-linux-build/action.yml @@ -0,0 +1,20 @@ +name: Bazel Linux Build +description: Common steps to build SwiftLint with Bazel on GitHub Linux runners +runs: + using: composite + steps: + - name: Create ci.bazelrc file + shell: bash + run: echo "$CI_BAZELRC_FILE_CONTENT" | base64 -d > ci.bazelrc + env: + CI_BAZELRC_FILE_CONTENT: ${{ env.CI_BAZELRC_FILE_CONTENT }} + - name: Apply patch + shell: bash + run: git apply --ignore-whitespace .bcr/patches/no-warnings-as-errors.patch + - name: Build SwiftLint with Bazel + shell: bash + run: | + export PATH="/usr/share/swift/usr/bin:$PATH" + bazel build --config release //:swiftlint + env: + CC: clang diff --git a/.github/actions/run-make/action.yml b/.github/actions/run-make/action.yml new file mode 100644 index 0000000000..0ff574904e --- /dev/null +++ b/.github/actions/run-make/action.yml @@ -0,0 +1,19 @@ +name: Run Make Rule +description: Runs a specified Makefile rule +inputs: + rule: + description: The Makefile rule to run + required: true + default: build +runs: + using: composite + steps: + - run: | + awk ' + $0 ~ "${{ inputs.rule }}:" { in_rule = 1; next } + in_rule && /^\t/ { print $0 } + in_rule && !/^\t/ { in_rule = 0 } + ' Makefile | while IFS= read -r command; do + eval "$command" + done + shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..b321e61622 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + bazel_linux: + name: Bazel, Linux, Swift 6.1 # pre-installed + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/bazel-linux-build + name: Build SwiftLint with Bazel + env: + CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} + + plugins_linux: + name: SPM plugins, Linux, Swift ${{ matrix.version }} + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + - image: swift:5.9-focal + version: '5.9' + - image: swift:5.10-noble + version: '5.10' + - image: swift:6.0-noble + version: '6.0' + - image: swift:6.1-noble + version: '6.1' + container: ${{ matrix.image }} + steps: + - uses: actions/checkout@v4 + - name: Build plugins + uses: ./.github/actions/run-make + with: + rule: spm_build_plugins diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 80dcff14a9..7f72c5a0e8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,8 +2,6 @@ name: Lint on: pull_request: - branches: - - '*' permissions: contents: read @@ -11,24 +9,18 @@ permissions: jobs: lint-swift: - name: Lint Swift + name: Swift runs-on: ubuntu-24.04 # "Noble Numbat" steps: - uses: actions/checkout@v4 - - name: Create ci.bazelrc file - run: echo "$CI_BAZELRC_FILE_CONTENT" | base64 -d > ci.bazelrc + - uses: ./.github/actions/bazel-linux-build + name: Build SwiftLint with Bazel env: CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} - name: Lint - run: | - export PATH="/usr/share/swift/usr/bin:$PATH" - git apply --ignore-whitespace .bcr/patches/no-warnings-as-errors.patch - bazel build --config release //:swiftlint - ./bazel-bin/swiftlint lint --reporter github-actions-logging --strict 2> /dev/null - env: - CC: clang + run: ./bazel-bin/swiftlint lint --reporter github-actions-logging --strict 2> /dev/null lint-markdown: - name: Lint Markdown + name: Markdown runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -40,7 +32,7 @@ jobs: CONTRIBUTING.md README.md lint-actions: - name: Lint Actions + name: Actions runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..37543dc15e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +env: + SKIP_INTEGRATION_TESTS: 'true' + +jobs: + spm_linux: + name: SPM, Linux, Swift 6.1 + runs-on: ubuntu-24.04 + container: swift:6.1-noble + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + key: ${{ runner.os }}-swift-spm-${{ hashFiles('Package.resolved') }} + restore-keys: ${{ runner.os }}-swift-spm- + path: .build + - name: Run SPM tests + uses: ./.github/actions/run-make + with: + rule: spm_test diff --git a/.gitignore b/.gitignore index ebc2d33e63..1ed5560422 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,7 @@ Packages/ bundle/ # Bazel -bazel-* +/bazel-* /MODULE.bazel.lock # Danger diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 77b8b5c14d..75578e801f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -5,39 +5,16 @@ variables: SKIP_INTEGRATION_TESTS: 'true' jobs: -- job: spm_linux - displayName: 'SPM, Linux : Swift 6.1' - pool: - vmImage: 'ubuntu-24.04' # "Noble Numbat" - container: swift:6.1-noble - steps: - - template: .azure/templates/run-make.yml - parameters: - rule: spm_test - -- job: bazel_linux - displayName: 'Bazel, Linux : Swift 6.0' - pool: - vmImage: 'ubuntu-24.04' - steps: - - script: | - export PATH="/usr/share/swift/usr/bin:$PATH" - git apply --ignore-whitespace .bcr/patches/no-warnings-as-errors.patch - bazel build :swiftlint - displayName: Build SwiftLint with Bazel - env: - CC: "clang" - - job: tests_macos displayName: 'Tests, macOS' strategy: maxParallel: '10' matrix: - '14 : Xcode 15.4': + '14, Xcode 15.4': image: 'macOS-14' xcode: '15.4' - # '14 : Xcode 16.3': Runs on Buildkite. - '15 : Xcode 16.4': + # '14, Xcode 16.3': Runs on Buildkite. + '15, Xcode 16.4': image: 'macOS-15' xcode: '16.4' pool: @@ -48,27 +25,6 @@ jobs: - script: make spm_test displayName: Run tests -- job: plugins_linux # Plugins shall be able to run on older Swift versions. - displayName: 'Plugins, Linux' - pool: - vmImage: 'ubuntu-24.04' # "Noble Numbat" - strategy: - maxParallel: '10' - matrix: - ': Swift 5.9': - image: swift:5.9-focal - ': Swift 5.10': - image: swift:5.10-noble - ': Swift 6.0': - image: swift:6.0-noble - ': Swift 6.1': - image: swift:6.1-noble - container: $[ variables['image'] ] - steps: - - template: .azure/templates/run-make.yml - parameters: - rule: spm_build_plugins - - job: Jazzy pool: vmImage: 'macOS-14' From ab7d1170307667085e3167ec8a6f82526b859ff0 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Tue, 24 Jun 2025 09:48:25 -0400 Subject: [PATCH 45/80] Migrate FileHeaderRule from SourceKit to SwiftSyntax (#6112) ## Summary Convert FileHeaderRule to use SwiftSyntax instead of SourceKit for improved performance and better handling of file header comments, shebangs, and doc comments. ## Key Technical Improvements - **Enhanced shebang support** properly skipping past `#!/usr/bin/env swift` lines - **Better comment type discrimination** excluding doc comments from header analysis - **Accurate position calculation** converting between UTF-8 and UTF-16 offsets for regex matching - **Improved trivia traversal** for comprehensive header comment collection - **SwiftLint command filtering** to exclude directive comments from header content ## Migration Details - Replaced `OptInRule` with `@SwiftSyntaxRule(optIn: true)` annotation - Implemented `ViolationsSyntaxVisitor` pattern for file-level analysis - Added logic to start header collection after shebang.endPosition if present - Distinguished between regular comments and doc comments (///, /** */) - Maintained UTF-16 offset calculations for NSRegularExpression compatibility - Added `skipDisableCommandTests: true` for SwiftSyntax disable command behavior - Removed unnecessary SourceKittenFramework import --- CHANGELOG.md | 1 + .../FileHeaderConfiguration.swift | 1 - .../Rules/Style/FileHeaderRule.swift | 228 ++++++++++++++---- .../FileHeaderRuleTests.swift | 9 +- 4 files changed, 178 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5daf95dc0..d6eab7f239 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * `accessibility_trait_for_button` * `closure_end_indentation` * `expiring_todo` + * `file_header` * `file_length` * `line_length` * `vertical_whitespace` diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift index e23e265ffd..e48ee1e6af 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileHeaderConfiguration.swift @@ -1,5 +1,4 @@ import Foundation -import SourceKittenFramework import SwiftLintCore struct FileHeaderConfiguration: SeverityBasedRuleConfiguration { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift index 866fa84269..ef867230af 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift @@ -1,7 +1,9 @@ import Foundation import SourceKittenFramework +import SwiftSyntax -struct FileHeaderRule: OptInRule { +@SwiftSyntaxRule(optIn: true) +struct FileHeaderRule: Rule { var configuration = FileHeaderConfiguration() static let description = RuleDescription( @@ -30,80 +32,200 @@ struct FileHeaderRule: OptInRule { """), ].skipWrappingInCommentTests() ) +} + +private struct ProcessTriviaResult { + let foundNonComment: Bool +} + +private extension FileHeaderRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + let headerRange = collectHeaderComments(from: node) + + let requiredRegex = configuration.requiredRegex(for: file) + + // If no header comments found + guard let headerRange else { + if requiredRegex != nil { + let violationPosition = node.shebang?.endPosition ?? node.position + violations.append(ReasonedRuleViolation( + position: violationPosition, + reason: requiredReason() + )) + } + return .skipChildren + } + + // Extract header content + guard let headerContent = extractHeaderContent(from: headerRange) else { + return .skipChildren + } - func validate(file: SwiftLintFile) -> [StyleViolation] { - var firstToken: SwiftLintSyntaxToken? - var lastToken: SwiftLintSyntaxToken? - var firstNonCommentToken: SwiftLintSyntaxToken? - - for token in file.syntaxTokensByLines.lazy.joined() { - guard let kind = token.kind, kind.isFileHeaderKind else { - // found a token that is not a comment, which means it's not the top of the file - // so we can just skip the remaining tokens - firstNonCommentToken = token - break + // Check patterns + checkForbiddenPattern(in: headerContent, startingAt: headerRange.start) + checkRequiredPattern(requiredRegex, in: headerContent, startingAt: headerRange.start) + + return .skipChildren + } + + private func collectHeaderComments( + from node: SourceFileSyntax + ) -> (start: AbsolutePosition, end: AbsolutePosition)? { + var firstHeaderCommentStart: AbsolutePosition? + var lastHeaderCommentEnd: AbsolutePosition? + + // Skip past shebang if present + var currentPosition = node.position + if let shebang = node.shebang { + currentPosition = shebang.endPosition } - // skip SwiftLint commands - guard !isSwiftLintCommand(token: token, file: file) else { - continue + // Collect header comments from tokens' trivia + for token in node.tokens(viewMode: .sourceAccurate) { + // Skip tokens before the start position (e.g., shebang) + if token.endPosition <= currentPosition { + continue + } + + let triviaResult = processTrivia( + token.leadingTrivia, + startingAt: ¤tPosition, + firstStart: &firstHeaderCommentStart, + lastEnd: &lastHeaderCommentEnd + ) + + if triviaResult.foundNonComment || token.tokenKind != .endOfFile { + break + } + + // Update current position past the token + currentPosition = token.endPositionBeforeTrailingTrivia + + // Process trailing trivia if it's EOF + if token.tokenKind == .endOfFile { + _ = processTrivia(token.trailingTrivia, + startingAt: ¤tPosition, + firstStart: &firstHeaderCommentStart, + lastEnd: &lastHeaderCommentEnd) + } } - if firstToken == nil { - firstToken = token + guard let start = firstHeaderCommentStart, + let end = lastHeaderCommentEnd, + start < end else { + return nil } - lastToken = token + + return (start: start, end: end) } - let requiredRegex = configuration.requiredRegex(for: file) + private func processTrivia(_ trivia: Trivia, + startingAt currentPosition: inout AbsolutePosition, + firstStart: inout AbsolutePosition?, + lastEnd: inout AbsolutePosition?) -> ProcessTriviaResult { + for piece in trivia { + let pieceStart = currentPosition + currentPosition += piece.sourceLength - var violationsOffsets = [Int]() - if let firstToken, let lastToken { - let start = firstToken.offset - let length = lastToken.offset + lastToken.length - firstToken.offset - let byteRange = ByteRange(location: start, length: length) - guard let range = file.stringView.byteRangeToNSRange(byteRange) else { - return [] + if isSwiftLintCommand(piece: piece) { + continue + } + + if piece.isComment && !piece.isDocComment { + if firstStart == nil { + firstStart = pieceStart + } + lastEnd = currentPosition + } else if !piece.isWhitespace { + return ProcessTriviaResult(foundNonComment: true) + } } + return ProcessTriviaResult(foundNonComment: false) + } + + private func extractHeaderContent(from range: (start: AbsolutePosition, end: AbsolutePosition)) -> String? { + let headerByteRange = ByteRange( + location: ByteCount(range.start.utf8Offset), + length: ByteCount(range.end.utf8Offset - range.start.utf8Offset) + ) + + return file.stringView.substringWithByteRange(headerByteRange) + } - if let regex = configuration.forbiddenRegex(for: file), - let firstMatch = regex.matches(in: file.contents, options: [], range: range).first { - violationsOffsets.append(firstMatch.range.location) + private func checkForbiddenPattern(in headerContent: String, startingAt headerStart: AbsolutePosition) { + guard + let forbiddenRegex = configuration.forbiddenRegex(for: file), + let firstMatch = forbiddenRegex.firstMatch( + in: headerContent, + options: [], + range: headerContent.fullNSRange + ) + else { + return } - if let regex = requiredRegex, - case let matches = regex.matches(in: file.contents, options: [], range: range), - matches.isEmpty { - violationsOffsets.append(file.stringView.location(fromByteOffset: start)) + // Calculate violation position + let matchLocationUTF16 = firstMatch.range.location + let headerPrefix = String(headerContent.utf16.prefix(matchLocationUTF16)) ?? "" + let utf8OffsetInHeader = headerPrefix.utf8.count + let violationPosition = AbsolutePosition(utf8Offset: headerStart.utf8Offset + utf8OffsetInHeader) + + violations.append(ReasonedRuleViolation( + position: violationPosition, + reason: forbiddenReason() + )) + } + + private func checkRequiredPattern(_ requiredRegex: NSRegularExpression?, + in headerContent: String, + startingAt headerStart: AbsolutePosition) { + guard + let requiredRegex, + requiredRegex.firstMatch(in: headerContent, options: [], range: headerContent.fullNSRange) == nil + else { + return } - } else if requiredRegex != nil { - let location = firstNonCommentToken.map { - Location(file: file, byteOffset: $0.offset) - } ?? Location(file: file.path, line: 1) - return [makeViolation(at: location)] + + violations.append(ReasonedRuleViolation( + position: headerStart, + reason: requiredReason() + )) } - return violationsOffsets.map { makeViolation(at: Location(file: file, characterOffset: $0)) } - } + private func isSwiftLintCommand(piece: TriviaPiece) -> Bool { + guard let text = piece.commentText else { return false } + return text.contains("swiftlint:") + } - private func isSwiftLintCommand(token: SwiftLintSyntaxToken, file: SwiftLintFile) -> Bool { - guard let range = file.stringView.byteRangeToNSRange(token.range) else { - return false + private func forbiddenReason() -> String { + "Header comments should be consistent with project patterns" } - return file.commands(in: range).isNotEmpty + private func requiredReason() -> String { + "Header comments should be consistent with project patterns" + } } +} - private func makeViolation(at location: Location) -> StyleViolation { - StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: location, - reason: "Header comments should be consistent with project patterns") +// Helper extensions +private extension TriviaPiece { + var isDocComment: Bool { + switch self { + case .docLineComment, .docBlockComment: + return true + default: + return false + } } -} -private extension SyntaxKind { - var isFileHeaderKind: Bool { - self == .comment || self == .commentURL + var commentText: String? { + switch self { + case .lineComment(let text), .blockComment(let text), + .docLineComment(let text), .docBlockComment(let text): + return text + default: + return nil + } } } diff --git a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift index d60354b42e..6cfceab5cd 100644 --- a/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileHeaderRuleTests.swift @@ -5,12 +5,6 @@ import XCTest private let fixturesDirectory = "\(TestResources.path())/FileHeaderRuleFixtures" final class FileHeaderRuleTests: SwiftLintTestCase { - override func invokeTest() { - CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) { - super.invokeTest() - } - } - private func validate(fileName: String, using configuration: Any) throws -> [StyleViolation] { let file = SwiftLintFile(path: fixturesDirectory.stringByAppendingPathComponent(fileName))! let rule = try FileHeaderRule(configuration: configuration) @@ -39,7 +33,8 @@ final class FileHeaderRuleTests: SwiftLintTestCase { verifyRule(description, ruleConfiguration: ["required_string": "**Header"], stringDoesntViolate: false, skipCommentTests: true, - testMultiByteOffsets: false, testShebang: false) + skipDisableCommandTests: true, testMultiByteOffsets: false, + testShebang: false) } func testFileHeaderWithRequiredPattern() { From f7f3caa50e4cf8a0ead5a936bbdeeef3c76e4213 Mon Sep 17 00:00:00 2001 From: JP Simard Date: Tue, 24 Jun 2025 21:12:32 -0400 Subject: [PATCH 46/80] Migrate TrailingWhitespaceRule from SourceKit to SwiftSyntax (#6117) ## Summary Convert TrailingWhitespaceRule to use SwiftSyntax instead of SourceKit for improved performance and better handling of trailing whitespace detection, especially within block comments. ## Key Technical Improvements - **Enhanced block comment detection** distinguishing between lines fully covered by block comments vs lines containing block comments with code - **Accurate whitespace detection** using CharacterSet.whitespaces for all Unicode whitespace characters, not just space and tab - **Improved comment handling** with proper detection of line-ending comments and multi-line block comment structures - **Better correction mechanism** using ViolationCorrection ranges instead of manual string reconstruction - **Line-based analysis** maintaining efficiency while providing precise violation positions ## Migration Details - Replaced `CorrectableRule` with `@SwiftSyntaxRule(correctable: true)` - Implemented `ViolationsSyntaxVisitor` pattern for line-based validation - Added `collectLinesFullyCoveredByBlockComments` to properly handle test framework comment wrapping scenarios - Distinguished between three comment scenarios: lines fully within block comments, full-line comments, and lines ending with comments - Maintained all configuration options (ignores_empty_lines, ignores_comments) - Preserved exact violation position reporting with UTF-8 offset calculations --- CHANGELOG.md | 1 + .../Rules/Style/TrailingWhitespaceRule.swift | 339 +++++++++++++++--- 2 files changed, 290 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6eab7f239..cc3c23998f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ * `file_header` * `file_length` * `line_length` + * `trailing_whitespace` * `vertical_whitespace` [JP Simard](https://github.com/jpsim) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift index 4e1e78d411..ff24517cdc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift @@ -1,7 +1,9 @@ import Foundation -import SourceKittenFramework +import SwiftLintCore +import SwiftSyntax -struct TrailingWhitespaceRule: CorrectableRule { +@SwiftSyntaxRule(correctable: true) +struct TrailingWhitespaceRule: Rule { var configuration = TrailingWhitespaceConfiguration() static let description = RuleDescription( @@ -14,77 +16,314 @@ struct TrailingWhitespaceRule: CorrectableRule { Example("let name: String //\n"), Example("let name: String // \n"), ], triggeringExamples: [ - Example("let name: String \n"), Example("/* */ let name: String \n") + Example("let name: String↓ \n"), Example("/* */ let name: String↓ \n") ], corrections: [ - Example("let name: String \n"): Example("let name: String\n"), - Example("/* */ let name: String \n"): Example("/* */ let name: String\n"), + Example("let name: String↓ \n"): Example("let name: String\n"), + Example("/* */ let name: String↓ \n"): Example("/* */ let name: String\n"), ] ) +} - func validate(file: SwiftLintFile) -> [StyleViolation] { - let filteredLines = file.lines.filter { - guard $0.content.hasTrailingWhitespace() else { return false } +private extension TrailingWhitespaceRule { + final class Visitor: ViolationsSyntaxVisitor { + // Pre-computed comment information for performance + private var linesFullyCoveredByBlockComments = Set() + private var linesEndingWithComment = Set() - let commentKinds = SyntaxKind.commentKinds - if configuration.ignoresComments, - let lastSyntaxKind = file.syntaxKindsByLines[$0.index].last, - commentKinds.contains(lastSyntaxKind) { - return false + override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind { + // Pre-compute all comment information in a single pass if needed + if configuration.ignoresComments { + precomputeCommentInformation(node) } - return !configuration.ignoresEmptyLines || - // If configured, ignore lines that contain nothing but whitespace (empty lines) - $0.content.trimmingCharacters(in: .whitespaces).isNotEmpty + // Process each line for trailing whitespace violations + for lineContents in file.lines { + let line = lineContents.content + let lineNumber = lineContents.index // 1-based + + // Calculate trailing whitespace info + guard let trailingWhitespaceInfo = line.trailingWhitespaceInfo() else { + continue // No trailing whitespace + } + + // Apply `ignoresEmptyLines` configuration + if configuration.ignoresEmptyLines && + line.trimmingCharacters(in: .whitespaces).isEmpty { + continue + } + + // Apply `ignoresComments` configuration + if configuration.ignoresComments { + // Check if line is fully within a block comment + if linesFullyCoveredByBlockComments.contains(lineNumber) { + continue + } + + // Check if line ends with a comment (using pre-computed info) + if linesEndingWithComment.contains(lineNumber) { + continue + } + } + + // Calculate violation position + let lineStartPos = locationConverter.position(ofLine: lineNumber, column: 1) + let violationStartOffset = line.utf8.count - trailingWhitespaceInfo.byteLength + let violationPosition = lineStartPos.advanced(by: violationStartOffset) + + let correctionEnd = lineStartPos.advanced(by: line.utf8.count) + + violations.append(ReasonedRuleViolation( + position: violationPosition, + correction: .init(start: violationPosition, end: correctionEnd, replacement: "") + )) + } + return .skipChildren } - return filteredLines.map { - StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: Location(file: file.path, line: $0.index)) + /// Pre-computes all comment information in a single pass for better performance + private func precomputeCommentInformation(_ node: SourceFileSyntax) { + // First, collect block comment information + collectLinesFullyCoveredByBlockComments(node) + + // Then, collect line comment ranges and determine which lines end with comments + let lineCommentRanges = collectLineCommentRanges(from: node) + determineLineEndingComments(using: lineCommentRanges) } - } - func correct(file: SwiftLintFile) -> Int { - let whitespaceCharacterSet = CharacterSet.whitespaces - var correctedLines = [String]() - var numberOfCorrections = 0 - for line in file.lines { - guard line.content.hasTrailingWhitespace() else { - correctedLines.append(line.content) - continue + /// Collects ranges of line comments organized by line number + private func collectLineCommentRanges(from node: SourceFileSyntax) -> [Int: [Range]] { + var lineCommentRanges: [Int: [Range]] = [:] + + for token in node.tokens(viewMode: .sourceAccurate) { + // Process leading trivia + var currentPos = token.position + for piece in token.leadingTrivia { + let pieceStart = currentPos + currentPos += piece.sourceLength + + if piece.isComment && !piece.isBlockComment { + let pieceStartLine = locationConverter.location(for: pieceStart).line + lineCommentRanges[pieceStartLine, default: []].append(pieceStart..]]) { + for lineNumber in 1...file.lines.count { + let line = file.lines[lineNumber - 1].content + + // Skip if no trailing whitespace + guard let trailingWhitespaceInfo = line.trailingWhitespaceInfo() else { + continue + } + + // Get the effective content (before trailing whitespace) + let effectiveContent = getEffectiveContent(from: line, removing: trailingWhitespaceInfo) + + // Check if the effective content ends with a comment + if checkIfContentEndsWithComment( + effectiveContent, + lineNumber: lineNumber, + lineCommentRanges: lineCommentRanges + ) { + linesEndingWithComment.insert(lineNumber) + } + } + } + + /// Gets the content of a line before its trailing whitespace + private func getEffectiveContent( + from line: String, + removing trailingWhitespaceInfo: TrailingWhitespaceInfo + ) -> String { + if trailingWhitespaceInfo.characterCount > 0 && line.count >= trailingWhitespaceInfo.characterCount { + return String(line.prefix(line.count - trailingWhitespaceInfo.characterCount)) + } + return "" + } + + /// Checks if the given content ends with a comment + private func checkIfContentEndsWithComment( + _ effectiveContent: String, + lineNumber: Int, + lineCommentRanges: [Int: [Range]] + ) -> Bool { + guard !effectiveContent.isEmpty, + let lastNonWhitespaceIdx = effectiveContent.lastIndex(where: { !$0.isWhitespace }) else { + return false + } + + // Calculate the byte position of the last non-whitespace character + let contentUpToLastChar = effectiveContent.prefix(through: lastNonWhitespaceIdx) + let byteOffsetToLastChar = contentUpToLastChar.utf8.count - 1 // -1 for position of char + let lineStartPos = locationConverter.position(ofLine: lineNumber, column: 1) + let lastNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToLastChar) + + // Check if this position falls within any comment range on this line + if let ranges = lineCommentRanges[lineNumber] { + for range in ranges { + if range.lowerBound <= lastNonWhitespacePos && lastNonWhitespacePos < range.upperBound { + return true + } + } } - let commentKinds = SyntaxKind.commentKinds - if configuration.ignoresComments, - let lastSyntaxKind = file.syntaxKindsByLines[line.index].last, - commentKinds.contains(lastSyntaxKind) { - correctedLines.append(line.content) - continue + return false + } + + /// Collects line numbers that are fully covered by block comments + private func collectLinesFullyCoveredByBlockComments(_ sourceFile: SourceFileSyntax) { + for token in sourceFile.tokens(viewMode: .sourceAccurate) { + var currentPos = token.position + + // Process leading trivia + for piece in token.leadingTrivia { + let pieceStartPos = currentPos + currentPos += piece.sourceLength + + if piece.isBlockComment { + markLinesFullyCoveredByBlockComment( + blockCommentStart: pieceStartPos, + blockCommentEnd: currentPos + ) + } + } + + // Advance past token content + currentPos = token.endPositionBeforeTrailingTrivia + + // Process trailing trivia + for piece in token.trailingTrivia { + let pieceStartPos = currentPos + currentPos += piece.sourceLength + + if piece.isBlockComment { + markLinesFullyCoveredByBlockComment( + blockCommentStart: pieceStartPos, + blockCommentEnd: currentPos + ) + } + } + } + } + + /// Marks lines that are fully covered by a block comment + private func markLinesFullyCoveredByBlockComment( + blockCommentStart: AbsolutePosition, + blockCommentEnd: AbsolutePosition + ) { + let startLocation = locationConverter.location(for: blockCommentStart) + let endLocation = locationConverter.location(for: blockCommentEnd) + + let startLine = startLocation.line + var endLine = endLocation.line + + // If comment ends at column 1, it actually ended on the previous line + if endLocation.column == 1 && endLine > startLine { + endLine -= 1 } - let correctedLine = line.content.bridge() - .trimmingTrailingCharacters(in: whitespaceCharacterSet) + for lineNum in startLine...endLine { + if lineNum <= 0 || lineNum > file.lines.count { continue } - if configuration.ignoresEmptyLines && correctedLine.isEmpty { - correctedLines.append(line.content) - continue + let lineInfo = file.lines[lineNum - 1] + let lineContent = lineInfo.content + let lineStartPos = locationConverter.position(ofLine: lineNum, column: 1) + + // Check if the line's non-whitespace content is fully within the block comment + if let firstNonWhitespaceIdx = lineContent.firstIndex(where: { !$0.isWhitespace }), + let lastNonWhitespaceIdx = lineContent.lastIndex(where: { !$0.isWhitespace }) { + // Line has non-whitespace content + // Calculate byte offsets (not character offsets) for AbsolutePosition + let contentBeforeFirstNonWS = lineContent.prefix(upTo: firstNonWhitespaceIdx) + let byteOffsetToFirstNonWS = contentBeforeFirstNonWS.utf8.count + let firstNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToFirstNonWS) + + let contentBeforeLastNonWS = lineContent.prefix(upTo: lastNonWhitespaceIdx) + let byteOffsetToLastNonWS = contentBeforeLastNonWS.utf8.count + let lastNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToLastNonWS) + + // Check if both first and last non-whitespace positions are within the comment + if firstNonWhitespacePos >= blockCommentStart && lastNonWhitespacePos < blockCommentEnd { + linesFullyCoveredByBlockComments.insert(lineNum) + } + } else { + // Line is all whitespace - check if it's within the comment bounds + let lineEndPos = lineStartPos.advanced(by: lineContent.utf8.count) + if lineStartPos >= blockCommentStart && lineEndPos <= blockCommentEnd { + linesFullyCoveredByBlockComments.insert(lineNum) + } + } } + } + } +} + +// Helper struct to return both character count and byte length for whitespace +private struct TrailingWhitespaceInfo { + let characterCount: Int + let byteLength: Int +} - if file.ruleEnabled(violatingRanges: [line.range], for: self).isEmpty { - correctedLines.append(line.content) - continue +private extension String { + func hasTrailingWhitespace() -> Bool { + if isEmpty { return false } + guard let lastScalar = unicodeScalars.last else { return false } + return CharacterSet.whitespaces.contains(lastScalar) + } + + /// Returns information about trailing whitespace (spaces and tabs only) + func trailingWhitespaceInfo() -> TrailingWhitespaceInfo? { + var charCount = 0 + var byteLen = 0 + for char in self.reversed() { + if char.isWhitespace && (char == " " || char == "\t") { // Only count spaces and tabs + charCount += 1 + byteLen += char.utf8.count + } else { + break } + } + return charCount > 0 ? TrailingWhitespaceInfo(characterCount: charCount, byteLength: byteLen) : nil + } - if line.content != correctedLine { - numberOfCorrections += 1 + func trimmingTrailingCharacters(in characterSet: CharacterSet) -> String { + var end = endIndex + while end > startIndex { + let index = index(before: end) + if !characterSet.contains(self[index].unicodeScalars.first!) { + break } - correctedLines.append(correctedLine) + end = index } - if numberOfCorrections > 0 { - // join and re-add trailing newline - file.write(correctedLines.joined(separator: "\n") + "\n") + return String(self[.. Date: Thu, 26 Jun 2025 04:30:32 +0900 Subject: [PATCH 47/80] Add new `excluded_paths` option to `file_name` rule (#6092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Mösch --- CHANGELOG.md | 7 ++ .../Rules/Idiomatic/FileNameRule.swift | 4 +- .../FileNameConfiguration.swift | 16 +++- .../BuiltInRulesTests/FileNameRuleTests.swift | 79 +++++++++++++++++++ .../default_rule_configurations.yml | 1 + 5 files changed, 104 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3c23998f..bb80040b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,13 @@ [JP Simard](https://github.com/jpsim) [Matt Pennig](https://github.com/pennig) +* Add `excluded_paths` option to the `file_name` rule. It allows to exclude complete file + paths from analysis. All entries are treated as regular expressions. A single match in + its full path is enough to ignore a file. This is different from the `excluded` option + that only accepts and checks against file names. + [Ueeek](https://github.com/Ueeek) + [#6066](https://github.com/realm/SwiftLint/issues/6066) + * Fix false positives of `redundant_discardable_let` rule in `@ViewBuilder` functions, `#Preview` macro bodies and preview providers when `ignore_swiftui_view_bodies` is enabled. diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift index 6c6ab9cf21..80f4c9fdbd 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift @@ -12,14 +12,14 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { func validate(file: SwiftLintFile) -> [StyleViolation] { guard let filePath = file.path, - case let fileName = filePath.bridge().lastPathComponent, - !configuration.excluded.contains(fileName) else { + !configuration.shouldExclude(filePath: filePath) else { return [] } let prefixRegex = regex("\\A(?:\(configuration.prefixPattern))") let suffixRegex = regex("(?:\(configuration.suffixPattern))\\z") + let fileName = filePath.bridge().lastPathComponent var typeInFileName = fileName.bridge().deletingPathExtension // Process prefix diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift index 7aa8be61b5..1dca2b089a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/FileNameConfiguration.swift @@ -7,7 +7,9 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) @ConfigurationElement(key: "excluded") - private(set) var excluded: Set = ["main.swift", "LinuxMain.swift"] + private(set) var excluded = Set(["main.swift", "LinuxMain.swift"]) + @ConfigurationElement(key: "excluded_paths") + private(set) var excludedPaths = Set() @ConfigurationElement(key: "prefix_pattern") private(set) var prefixPattern = "" @ConfigurationElement(key: "suffix_pattern") @@ -17,3 +19,15 @@ struct FileNameConfiguration: SeverityBasedRuleConfiguration { @ConfigurationElement(key: "require_fully_qualified_names") private(set) var requireFullyQualifiedNames = false } + +extension FileNameConfiguration { + func shouldExclude(filePath: String) -> Bool { + let fileName = filePath.bridge().lastPathComponent + if excluded.contains(fileName) { + return true + } + return excludedPaths.contains { + $0.regex.firstMatch(in: filePath, range: filePath.fullNSRange) != nil + } + } +} diff --git a/Tests/BuiltInRulesTests/FileNameRuleTests.swift b/Tests/BuiltInRulesTests/FileNameRuleTests.swift index 9b4e528681..d81ede7ffa 100644 --- a/Tests/BuiltInRulesTests/FileNameRuleTests.swift +++ b/Tests/BuiltInRulesTests/FileNameRuleTests.swift @@ -7,6 +7,7 @@ private let fixturesDirectory = "\(TestResources.path())/FileNameRuleFixtures" final class FileNameRuleTests: SwiftLintTestCase { private func validate(fileName: String, excluded: [String]? = nil, + excludedPaths: [String]? = nil, prefixPattern: String? = nil, suffixPattern: String? = nil, nestedTypeSeparator: String? = nil, @@ -18,6 +19,9 @@ final class FileNameRuleTests: SwiftLintTestCase { if let excluded { configuration["excluded"] = excluded } + if let excludedPaths { + configuration["excluded_paths"] = excludedPaths + } if let prefixPattern { configuration["prefix_pattern"] = prefixPattern } @@ -130,4 +134,79 @@ final class FileNameRuleTests: SwiftLintTestCase { ).isEmpty ) } + + func testExcludedDoesntSupportRegex() { + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [".*"] + ).isNotEmpty + ) + } + + func testExcludedPathPatternsSupportRegex() { + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*"] + ).isEmpty + ) + + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*.swift"] + ).isEmpty + ) + + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*/FileNameRuleFixtures/.*"] + ).isEmpty + ) + } + + func testExcludedPathPatternsWithRegexDoesntMatch() { + XCTAssert( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: [".*/OtherFolder/.*", "MAIN\\.swift"] + ).isNotEmpty + ) + } + + func testInvalidRegex() { + XCTAssertThrowsError( + try validate( + fileName: "NSString+Extension.swift", + excluded: [], + excludedPaths: ["("], + prefixPattern: "", + suffixPattern: "" + ) + ) + } + + func testExcludedPathPatternsWithMultipleRegexs() { + XCTAssertThrowsError( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: ["/FileNameRuleFixtures/.*", "("] + ) + ) + + XCTAssertThrowsError( + try validate( + fileName: "main.swift", + excluded: [], + excludedPaths: ["/FileNameRuleFixtures/.*", "(", ".*.swift"] + ) + ) + } } diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 981d667680..b3171d833a 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -377,6 +377,7 @@ file_length: file_name: severity: warning excluded: ["LinuxMain.swift", "main.swift"] + excluded_paths: [] prefix_pattern: "" suffix_pattern: "\+.*" nested_type_separator: "." From 17c77cd1f86a6ea31ea95735774dc1572270ff1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 25 Jun 2025 21:39:45 +0200 Subject: [PATCH 48/80] Print workspace directory only in debug builds (#6137) --- CHANGELOG.md | 5 +++++ .../SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift | 2 ++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb80040b52..375603d4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ * SwiftLint now requires macOS 13 or higher to run. [JP Simard](https://github.com/jpsim) + +* In `SwiftLintBuildToolPlugin`, print the content of the `BUILD_WORKSPACE_DIRECTORY` + environment variable only in debug builds. + [SimplyDanny](https://github.com/SimplyDanny) + [#6135](https://github.com/realm/SwiftLint/issues/6135) ### Experimental diff --git a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift index 211e28a21a..5e70a37c32 100644 --- a/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift +++ b/Plugins/SwiftLintBuildToolPlugin/SwiftLintBuildToolPlugin.swift @@ -50,7 +50,9 @@ struct SwiftLintBuildToolPlugin: BuildToolPlugin { return [] } // Outputs the environment to the build log for reference. + #if DEBUG print("Environment:", environment) + #endif let arguments: [String] = [ "lint", "--quiet", From 24437220be336738549312e34a3d834d645adc68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 25 Jun 2025 21:43:42 +0200 Subject: [PATCH 49/80] Use path pattern to exclude generated tests in config (#6138) --- .swiftlint.yml | 2 ++ Source/swiftlint-dev/Rules+Register.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_01.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_02.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_03.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_04.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_05.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_06.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_07.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_08.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_09.swift | 3 +-- Tests/GeneratedTests/GeneratedTests_10.swift | 3 +-- 12 files changed, 13 insertions(+), 22 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index dcb93c99d8..a4d6836284 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -74,6 +74,8 @@ file_name: - RuleConfigurationMacros.swift - SwiftSyntax+SwiftLint.swift - TestHelpers.swift + excluded_paths: + - Tests/GeneratedTests/GeneratedTests_\d\d\.swift final_test_case: *unit_test_configuration function_body_length: 60 identifier_name: diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index 30e3d9eac5..0f35968fdc 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -130,8 +130,7 @@ private extension SwiftLintDev.Rules.Register { private func generateSwiftTestFileContent(forTestClasses testClassesString: String) -> String { """ // GENERATED FILE. DO NOT EDIT! - // swiftlint:disable:previous file_name - // swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_01.swift b/Tests/GeneratedTests/GeneratedTests_01.swift index 40b4b1cf14..bcb0d4fcd6 100644 --- a/Tests/GeneratedTests/GeneratedTests_01.swift +++ b/Tests/GeneratedTests/GeneratedTests_01.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_02.swift b/Tests/GeneratedTests/GeneratedTests_02.swift index fd0b026a38..4e985d0744 100644 --- a/Tests/GeneratedTests/GeneratedTests_02.swift +++ b/Tests/GeneratedTests/GeneratedTests_02.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_03.swift b/Tests/GeneratedTests/GeneratedTests_03.swift index ca7f827cfe..3e290731ec 100644 --- a/Tests/GeneratedTests/GeneratedTests_03.swift +++ b/Tests/GeneratedTests/GeneratedTests_03.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_04.swift b/Tests/GeneratedTests/GeneratedTests_04.swift index a395ee79b1..2d6a1a273f 100644 --- a/Tests/GeneratedTests/GeneratedTests_04.swift +++ b/Tests/GeneratedTests/GeneratedTests_04.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_05.swift b/Tests/GeneratedTests/GeneratedTests_05.swift index bc12d8c63f..20c7fe2358 100644 --- a/Tests/GeneratedTests/GeneratedTests_05.swift +++ b/Tests/GeneratedTests/GeneratedTests_05.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift index 00ebadd755..b3c591b85f 100644 --- a/Tests/GeneratedTests/GeneratedTests_06.swift +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index 867cf61bf7..e9fc49379c 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index 5bd4603bc3..043afd79ff 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index 5940073b06..d200648e31 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index 11bf89d7e0..e2ee8c317c 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -1,6 +1,5 @@ // GENERATED FILE. DO NOT EDIT! -// swiftlint:disable:previous file_name -// swiftlint:disable:previous blanket_disable_command superfluous_disable_command + // swiftlint:disable:next blanket_disable_command superfluous_disable_command // swiftlint:disable file_length single_test_class type_name From d4fbe69e7bd501dbe220df5ad4df2f76ea65d0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Thu, 26 Jun 2025 21:23:26 +0200 Subject: [PATCH 50/80] Report remaining fixed and new violations (#6140) --- tools/oss-check | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/oss-check b/tools/oss-check index bb91df6bda..cd8bf6f032 100755 --- a/tools/oss-check +++ b/tools/oss-check @@ -297,6 +297,7 @@ def diff_and_report_changes_to_danger severity_changed = [] rule_id_changed = [] column_changed = [] + remaining_violations = [] new_violations.each do |line| fixed = fixed_violations.find { |other| line.equal_to?(other, [:message]) } @@ -315,8 +316,11 @@ def diff_and_report_changes_to_danger if fixed next column_changed << Change.new(:column, line, fixed) end + remaining_violations << line end + remaining_fixed = fixed_violations - (message_changed + severity_changed + rule_id_changed + column_changed).map(&:old) + # Print new and fixed violations to be processed by Danger. new_violations.each { |line| warn "This PR introduced a violation in #{repo.name}: #{line.to_full_message_with_linked_path(repo)}" @@ -346,13 +350,13 @@ def diff_and_report_changes_to_danger summary.puts column_changed.each { |change| change.print_as_diff(repo, summary) } summary.puts - summary.puts "### Fixed violations (#{fixed_violations.count})" + summary.puts "### Other fixed violations (#{remaining_fixed.count})" summary.puts - fixed_violations.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } + remaining_fixed.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } summary.puts - summary.puts "### New violations (#{new_violations.count})" + summary.puts "### Other new violations (#{remaining_violations.count})" summary.puts - new_violations.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } + remaining_violations.each { |violation| summary.puts "- #{violation.to_full_message_with_linked_path(repo)}" } summary.puts summary.string From 546f71bb2dd881c021ac60b5a14e66e4fc4aac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 27 Jun 2025 10:28:24 +0200 Subject: [PATCH 51/80] Add additional newline to first token in file (#6141) For all first tokens in a line the newline character causing the new line is part of the leading trivia. The only exception is the very first token in a file because at the beginning of a file, there is no previous line that needs to be broken. --- .../Rules/Style/VerticalWhitespaceRule.swift | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 4e1c827e04..956b5a28a0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -16,12 +16,28 @@ struct VerticalWhitespaceRule: Rule { Example("/* bcs \n\n\n\n*/"), Example("// bca \n\n"), Example("class CCCC {\n \n}"), + Example(""" + // comment + + import Foundation + """), + Example(""" + + // comment + + import Foundation + """), ], triggeringExamples: [ Example("let aaaa = 0\n\n\n"), Example("struct AAAA {}\n\n\n\n"), Example("class BBBB {}\n\n\n"), Example("class CCCC {\n \n \n}"), + Example(""" + + + import Foundation + """), ], corrections: [ Example("let b = 0\n\n\nclass AAA {}\n"): Example("let b = 0\n\nclass AAA {}\n"), @@ -34,7 +50,13 @@ struct VerticalWhitespaceRule: Rule { private extension VerticalWhitespaceRule { final class Visitor: ViolationsSyntaxVisitor { + /// The number of additional newlines to expect before the first token. + private var firstTokenAdditionalNewlines = 1 + override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind { + // Reset immediately. Only the first token has an additional leading newline. + defer { firstTokenAdditionalNewlines = 0 } + // The strategy here is to keep track of the position of the _first_ violating newline // in each consecutive run, and report the violation when the run _ends_. @@ -47,7 +69,7 @@ private extension VerticalWhitespaceRule { var violationPosition: AbsolutePosition? func process(_ count: Int, _ offset: Int) { - for _ in 0.. configuration.maxEmptyLines && violationPosition == nil { violationPosition = currentPosition } @@ -65,6 +87,8 @@ private extension VerticalWhitespaceRule { case .spaces, .tabs: currentPosition += piece.sourceLength default: + // A comment breaks the chain of newlines. + firstTokenAdditionalNewlines = 0 if let violationPosition { report(violationPosition, consecutiveNewlines) } From 929f0fc5c233a61518506688aa1608e096d1e8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 27 Jun 2025 10:29:30 +0200 Subject: [PATCH 52/80] Add warmup step to OSS check (#6134) --- tools/oss-check | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/oss-check b/tools/oss-check index cd8bf6f032..75f0c64fb6 100755 --- a/tools/oss-check +++ b/tools/oss-check @@ -420,6 +420,12 @@ def report_binary_size end end +def warmup + %w[branch main].each do |branch| + perform("../builds/swiftlint-#{branch} lint --no-cache --enable-all-rules", dir: "#{$working_dir}/Aerial") + end +end + ################################ # Script ################################ @@ -478,6 +484,7 @@ unless @options[:force] end setup_repos +warmup %w[branch main].each do |branch| generate_reports(branch) From de613ba8b721f5d205ef835cbdbbddccd713f774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sun, 29 Jun 2025 18:40:11 +0200 Subject: [PATCH 53/80] Prepare rule for SwiftSyntax 6.2 (#6144) --- .../Rules/Lint/YodaConditionRule.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift index 8289db7f38..8c85e9f566 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift @@ -21,7 +21,7 @@ struct YodaConditionRule: Rule { Example("if foo == nil {}"), Example("if flags & 1 == 1 {}"), Example("if true {}", excludeFromDocumentation: true), - Example("if true == false || b, 2 != 3, {}", excludeFromDocumentation: true), + Example("if true == false || b, 2 != 3 {}", excludeFromDocumentation: true), ], triggeringExamples: [ Example("if ↓42 == foo {}"), @@ -72,20 +72,20 @@ private extension YodaConditionRule { guard let operatorIndex = children.index(of: comparisonOperator) else { continue } - let rhsIdx = children.index(operatorIndex, offsetBy: 1) + let rhsIdx = children.index(after: operatorIndex) if children[rhsIdx].isLiteral { - let afterRhsIndex = children.index(after: rhsIdx) - guard children.endIndex != rhsIdx, afterRhsIndex != nil else { + guard children.endIndex != children.index(after: rhsIdx) else { // This is already the end of the expression. continue } + let afterRhsIndex = children.index(after: rhsIdx) if children[afterRhsIndex].isLogicalBinaryOperator { // Next token is an operator with weaker binding. Thus, the literal is unique on the // right-hand side of the comparison operator. continue } } - let lhsIdx = children.index(operatorIndex, offsetBy: -1) + let lhsIdx = children.index(before: operatorIndex) let lhs = children[lhsIdx] if lhs.isLiteral, children.startIndex == lhsIdx || children[children.index(before: lhsIdx)].isLogicalBinaryOperator { From fa61ea704cfcb87ab1241852f2a6abeb3240ba41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Tue, 1 Jul 2025 09:49:23 +0200 Subject: [PATCH 54/80] Fix test expectation (#6147) --- Tests/FrameworkTests/SwiftVersionTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/FrameworkTests/SwiftVersionTests.swift b/Tests/FrameworkTests/SwiftVersionTests.swift index 30575af129..c62735fa24 100644 --- a/Tests/FrameworkTests/SwiftVersionTests.swift +++ b/Tests/FrameworkTests/SwiftVersionTests.swift @@ -3,8 +3,8 @@ import XCTest final class SwiftVersionTests: SwiftLintTestCase { func testDetectSwiftVersion() { -#if compiler(>=6.2) - let version = "6.2" +#if compiler(>=6.2.0) + let version = "6.2.0" #elseif compiler(>=6.1.2) let version = "6.1.2" #elseif compiler(>=6.1.1) From 14edabdee8728d489d9ccd6873c128af5b67944c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Tue, 1 Jul 2025 23:41:48 +0200 Subject: [PATCH 55/80] Enable unnecessarily disabled rule (#6150) --- Source/swiftlint-dev/Rules+Register.swift | 2 +- Tests/GeneratedTests/GeneratedTests_01.swift | 2 +- Tests/GeneratedTests/GeneratedTests_02.swift | 2 +- Tests/GeneratedTests/GeneratedTests_03.swift | 2 +- Tests/GeneratedTests/GeneratedTests_04.swift | 2 +- Tests/GeneratedTests/GeneratedTests_05.swift | 2 +- Tests/GeneratedTests/GeneratedTests_06.swift | 2 +- Tests/GeneratedTests/GeneratedTests_07.swift | 2 +- Tests/GeneratedTests/GeneratedTests_08.swift | 2 +- Tests/GeneratedTests/GeneratedTests_09.swift | 2 +- Tests/GeneratedTests/GeneratedTests_10.swift | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Source/swiftlint-dev/Rules+Register.swift b/Source/swiftlint-dev/Rules+Register.swift index 0f35968fdc..269a03abf3 100644 --- a/Source/swiftlint-dev/Rules+Register.swift +++ b/Source/swiftlint-dev/Rules+Register.swift @@ -132,7 +132,7 @@ private extension SwiftLintDev.Rules.Register { // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command - // swiftlint:disable file_length single_test_class type_name + // swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_01.swift b/Tests/GeneratedTests/GeneratedTests_01.swift index bcb0d4fcd6..39903d1af5 100644 --- a/Tests/GeneratedTests/GeneratedTests_01.swift +++ b/Tests/GeneratedTests/GeneratedTests_01.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_02.swift b/Tests/GeneratedTests/GeneratedTests_02.swift index 4e985d0744..c8d05b2749 100644 --- a/Tests/GeneratedTests/GeneratedTests_02.swift +++ b/Tests/GeneratedTests/GeneratedTests_02.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_03.swift b/Tests/GeneratedTests/GeneratedTests_03.swift index 3e290731ec..d90a7c1169 100644 --- a/Tests/GeneratedTests/GeneratedTests_03.swift +++ b/Tests/GeneratedTests/GeneratedTests_03.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_04.swift b/Tests/GeneratedTests/GeneratedTests_04.swift index 2d6a1a273f..4cdd72ea8c 100644 --- a/Tests/GeneratedTests/GeneratedTests_04.swift +++ b/Tests/GeneratedTests/GeneratedTests_04.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_05.swift b/Tests/GeneratedTests/GeneratedTests_05.swift index 20c7fe2358..751aa6087c 100644 --- a/Tests/GeneratedTests/GeneratedTests_05.swift +++ b/Tests/GeneratedTests/GeneratedTests_05.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift index b3c591b85f..2155d6d695 100644 --- a/Tests/GeneratedTests/GeneratedTests_06.swift +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index e9fc49379c..22605a9a9f 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index 043afd79ff..e25c8f0d8f 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index d200648e31..be7d5dd42d 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index e2ee8c317c..3d0e2b5cbd 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -1,7 +1,7 @@ // GENERATED FILE. DO NOT EDIT! // swiftlint:disable:next blanket_disable_command superfluous_disable_command -// swiftlint:disable file_length single_test_class type_name +// swiftlint:disable single_test_class type_name @testable import SwiftLintBuiltInRules @testable import SwiftLintCore From 097bef27ef35c6a6c1ce37287cbca79974caed95 Mon Sep 17 00:00:00 2001 From: Rodion Ivashkov Date: Thu, 3 Jul 2025 00:04:45 +0300 Subject: [PATCH 56/80] Fix configuration handling in `multiline_parameters` for stricter validation (#6148) --- CHANGELOG.md | 4 +++ .../Rules/Style/MultilineParametersRule.swift | 26 ++++++++++++------- .../MultilineParametersRuleExamples.swift | 15 +++++++++-- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 375603d4e8..f2f4a0f4d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,10 @@ [kaseken](https://github.com/kaseken) [#6063](https://github.com/realm/SwiftLint/issues/6063) +* Improve `multiline_parameters` rule to correctly support + `max_number_of_single_line_parameters` and detect mixed formatting. + [GandaLF2006](https://github.com/GandaLF2006) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift index 7d715364a8..7ff00199d6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRule.swift @@ -35,26 +35,32 @@ private extension MultilineParametersRule { } var numberOfParameters = 0 - var linesWithParameters = Set() + var linesWithParameters: Set = [] + var hasMultipleParametersOnSameLine = false for position in parameterPositions { let line = locationConverter.location(for: position).line - linesWithParameters.insert(line) + + if !linesWithParameters.insert(line).inserted { + hasMultipleParametersOnSameLine = true + } + numberOfParameters += 1 } - if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters, - configuration.allowsSingleLine, - numberOfParameters > maxNumberOfSingleLineParameters { - return true - } + if linesWithParameters.count == 1 { + guard configuration.allowsSingleLine else { + return numberOfParameters > 1 + } + + if let maxNumberOfSingleLineParameters = configuration.maxNumberOfSingleLineParameters { + return numberOfParameters > maxNumberOfSingleLineParameters + } - guard linesWithParameters.count > (configuration.allowsSingleLine ? 1 : 0), - numberOfParameters != linesWithParameters.count else { return false } - return true + return hasMultipleParametersOnSameLine } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift index 4abcb60e39..250713e900 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersRuleExamples.swift @@ -199,11 +199,20 @@ internal struct MultilineParametersRuleExamples { """, configuration: ["allows_single_line": false]), Example("func foo(param1: Int, param2: Bool, param3: [String]) { }", configuration: ["max_number_of_single_line_parameters": 3]), + Example("func foo(param1: Int, param2: Bool) { }", + configuration: ["max_number_of_single_line_parameters": 2]), Example(""" func foo(param1: Int, param2: Bool, param3: [String]) { } """, configuration: ["max_number_of_single_line_parameters": 3]), + Example(""" + func foo( + param1: Int, + param2: Bool, + param3: [String] + ) { } + """, configuration: ["max_number_of_single_line_parameters": 2]), ] static let triggeringExamples: [Example] = [ @@ -348,7 +357,9 @@ internal struct MultilineParametersRuleExamples { Example(""" func ↓foo(param1: Int, param2: Bool, param3: [String]) { } - """, - configuration: ["max_number_of_single_line_parameters": 3]), + """, configuration: ["max_number_of_single_line_parameters": 3]), + Example(""" + func ↓foo(param1: Int, param2: Bool, param3: [String]) { } + """, configuration: ["max_number_of_single_line_parameters": 2]), ] } From 599e51a5a2aabe8fd2eb0577423678012e921b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 2 Jul 2025 23:50:53 +0200 Subject: [PATCH 57/80] Format code (#6151) --- .../Rules/Idiomatic/ExplicitInitRule.swift | 4 +- .../Rules/Idiomatic/FileNameNoSpaceRule.swift | 6 +-- .../Rules/Idiomatic/FileNameRule.swift | 12 ++--- .../Rules/Idiomatic/GenericTypeNameRule.swift | 2 +- .../Rules/Idiomatic/ObjectLiteralRule.swift | 4 +- .../RedundantObjcAttributeRule.swift | 10 ++--- .../Idiomatic/RedundantVoidReturnRule.swift | 2 +- .../Rules/Idiomatic/TypeNameRule.swift | 2 +- .../Idiomatic/UnneededBreakInSwitchRule.swift | 14 +++--- .../Idiomatic/UnusedEnumeratedRule.swift | 6 +-- .../VoidFunctionInTernaryConditionRule.swift | 6 +-- .../Idiomatic/XCTSpecificMatcherRule.swift | 2 +- .../Lint/AccessibilityLabelForImageRule.swift | 4 +- ...cessibilityLabelForImageRuleExamples.swift | 2 +- .../AccessibilityTraitForButtonRule.swift | 4 +- .../Rules/Lint/ArrayInitRule.swift | 2 +- .../Rules/Lint/AsyncWithoutAwaitRule.swift | 2 +- .../Lint/BlanketDisableCommandRule.swift | 2 +- .../Rules/Lint/CompilerProtocolInitRule.swift | 2 +- .../Rules/Lint/DeploymentTargetRule.swift | 6 +-- .../Rules/Lint/MissingDocsRule.swift | 32 ++++++------- .../Rules/Lint/OrphanedDocCommentRule.swift | 2 +- .../PrivateSwiftUIStatePropertyRule.swift | 8 ++-- .../Rules/Lint/QuickDiscouragedCallRule.swift | 39 ++++++++-------- .../Rules/Lint/TodoRule.swift | 8 ++-- .../Rules/Lint/TypesafeArrayInitRule.swift | 16 +++---- .../Rules/Lint/UnneededOverrideRule.swift | 2 +- .../Rules/Lint/UnusedDeclarationRule.swift | 10 ++--- .../Rules/Lint/UnusedImportRule.swift | 6 +-- .../Rules/Lint/UnusedSetterValueRule.swift | 2 +- .../Rules/Lint/WeakDelegateRule.swift | 4 +- .../Rules/Lint/YodaConditionRule.swift | 6 +-- .../Rules/Metrics/LineLengthRule.swift | 4 +- .../Rules/Metrics/TypeBodyLengthRule.swift | 4 +- .../Rules/Performance/FirstWhereRule.swift | 4 +- .../Rules/Performance/LastWhereRule.swift | 4 +- .../Rules/Performance/ReduceIntoRule.swift | 6 +-- .../DeploymentTargetConfiguration.swift | 16 +++---- .../UnusedImportConfiguration.swift | 6 +-- .../Rules/Style/ClosureSpacingRule.swift | 6 +-- .../Rules/Style/CollectionAlignmentRule.swift | 2 +- .../Rules/Style/CommaInheritanceRule.swift | 2 +- .../Style/ContrastedOpeningBraceRule.swift | 2 +- .../Rules/Style/ControlStatementRule.swift | 2 +- .../Rules/Style/ExplicitSelfRule.swift | 8 ++-- .../Rules/Style/LeadingWhitespaceRule.swift | 6 +-- .../LiteralExpressionEndIndentationRule.swift | 26 +++++------ .../Style/MultilineFunctionChainsRule.swift | 45 +++++++++---------- .../Style/MultilineLiteralBracketsRule.swift | 2 +- .../MultilineParametersBracketsRule.swift | 2 +- .../Rules/Style/NumberSeparatorRule.swift | 10 ++--- .../Rules/Style/OpeningBraceRule.swift | 26 +++++------ .../Style/OperatorUsageWhitespaceRule.swift | 6 +-- .../Rules/Style/SelfBindingRule.swift | 4 +- .../Rules/Style/ShorthandArgumentRule.swift | 2 +- .../Rules/Style/SortedImportsRule.swift | 5 ++- .../Rules/Style/StatementPositionRule.swift | 4 +- .../Rules/Style/TrailingCommaRule.swift | 2 +- .../Rules/Style/TrailingWhitespaceRule.swift | 2 +- .../VerticalWhitespaceBetweenCasesRule.swift | 2 +- .../Extensions/Dictionary+SwiftLint.swift | 2 +- .../Extensions/Request+SwiftLint.swift | 2 +- .../Extensions/String+SwiftLint.swift | 4 +- .../Extensions/SwiftLintFile+Regex.swift | 2 +- .../Extensions/SwiftSyntax+SwiftLint.swift | 4 +- Source/SwiftLintCore/Models/Baseline.swift | 7 ++- .../ChildOptionSeverityConfiguration.swift | 2 +- Source/SwiftLintCore/Models/Command.swift | 6 +-- .../Models/RuleDescription.swift | 2 +- .../SwiftLintCore/Models/SwiftVersion.swift | 2 +- Source/SwiftLintCore/Protocols/Rule.swift | 24 +++++----- .../RegexConfiguration.swift | 4 +- .../Visitors/CodeBlockVisitor.swift | 2 +- .../CompilerArgumentsExtractor.swift | 4 +- .../Configuration+CommandLine.swift | 4 +- .../Configuration/Configuration+Parsing.swift | 4 +- .../LintOrAnalyzeCommand.swift | 7 ++- .../LintableFilesVisitor.swift | 4 +- Source/SwiftLintFramework/Models/Linter.swift | 2 +- .../Models/LinterCache.swift | 6 +-- Tests/FrameworkTests/CustomRulesTests.swift | 2 +- Tests/FrameworkTests/GlobTests.swift | 2 +- Tests/TestHelpers/TestHelpers.swift | 28 ++++++------ 83 files changed, 280 insertions(+), 285 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift index 18f65633b0..336a8243c1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ExplicitInitRule.swift @@ -238,11 +238,11 @@ private extension ExprSyntax { return true } if let expr = self.as(MemberAccessExprSyntax.self), - expr.description.split(separator: ".").allSatisfy(\.startsWithUppercase) { + expr.description.split(separator: ".").allSatisfy(\.startsWithUppercase) { return true } if let expr = self.as(GenericSpecializationExprSyntax.self)?.expression.as(DeclReferenceExprSyntax.self), - expr.baseName.text.startsWithUppercase { + expr.baseName.text.startsWithUppercase { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift index 892ca60313..0a5d60e549 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameNoSpaceRule.swift @@ -13,9 +13,9 @@ struct FileNameNoSpaceRule: OptInRule, SourceKitFreeRule { func validate(file: SwiftLintFile) -> [StyleViolation] { guard let filePath = file.path, - case let fileName = filePath.bridge().lastPathComponent, - !configuration.excluded.contains(fileName), - fileName.rangeOfCharacter(from: .whitespaces) != nil else { + case let fileName = filePath.bridge().lastPathComponent, + !configuration.excluded.contains(fileName), + fileName.rangeOfCharacter(from: .whitespaces) != nil else { return [] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift index 80f4c9fdbd..f4651b9ef0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/FileNameRule.swift @@ -24,13 +24,13 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { // Process prefix if let match = prefixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange), - let range = typeInFileName.nsrangeToIndexRange(match.range) { + let range = typeInFileName.nsrangeToIndexRange(match.range) { typeInFileName.removeSubrange(range) } // Process suffix if let match = suffixRegex.firstMatch(in: typeInFileName, options: [], range: typeInFileName.fullNSRange), - let range = typeInFileName.nsrangeToIndexRange(match.range) { + let range = typeInFileName.nsrangeToIndexRange(match.range) { typeInFileName.removeSubrange(range) } @@ -38,10 +38,10 @@ struct FileNameRule: OptInRule, SourceKitFreeRule { let allDeclaredTypeNames = TypeNameCollectingVisitor( requireFullyQualifiedNames: configuration.requireFullyQualifiedNames ) - .walk(tree: file.syntaxTree, handler: \.names) - .map { - $0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator) - } + .walk(tree: file.syntaxTree, handler: \.names) + .map { + $0.replacingOccurrences(of: ".", with: configuration.nestedTypeSeparator) + } guard allDeclaredTypeNames.isNotEmpty, !allDeclaredTypeNames.contains(typeInFileName) else { return [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift index 91502f3403..3001e30cee 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/GenericTypeNameRule.swift @@ -67,7 +67,7 @@ private extension GenericTypeNameRule { ) ) } else if let caseCheckSeverity = configuration.validatesStartWithLowercase.severity, - !String(name[name.startIndex]).isUppercase() { + !String(name[name.startIndex]).isUppercase() { violations.append( ReasonedRuleViolation( position: node.positionAfterSkippingLeadingTrivia, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift index b68a8914fc..bd75545237 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/ObjectLiteralRule.swift @@ -62,8 +62,8 @@ private extension ObjectLiteralRule { private func isColorInit(node: FunctionCallExprSyntax, name: String) -> Bool { guard inits(forClasses: ["UIColor", "NSColor"]).contains(name), case let argumentsNames = node.arguments.compactMap(\.label?.text), - argumentsNames == ["red", "green", "blue", "alpha"] || argumentsNames == ["white", "alpha"] else { - return false + argumentsNames == ["red", "green", "blue", "alpha"] || argumentsNames == ["white", "alpha"] else { + return false } return node.arguments.allSatisfy(\.expression.canBeExpressedAsColorLiteralParams) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift index 6263405e0b..ed18f588c8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantObjcAttributeRule.swift @@ -63,7 +63,7 @@ private extension Syntax { return true } if let variableDecl = self.as(VariableDeclSyntax.self), - variableDecl.bindings.allSatisfy({ $0.accessorBlock == nil }) { + variableDecl.bindings.allSatisfy({ $0.accessorBlock == nil }) { return true } return false @@ -93,12 +93,12 @@ private extension AttributeListSyntax { return nil } if parent?.isFunctionOrStoredProperty == true, - let parentClassDecl = parent?.parent?.parent?.parent?.parent?.as(ClassDeclSyntax.self), - parentClassDecl.attributes.contains(attributeNamed: "objcMembers") { + let parentClassDecl = parent?.parent?.parent?.parent?.parent?.as(ClassDeclSyntax.self), + parentClassDecl.attributes.contains(attributeNamed: "objcMembers") { return parent?.functionOrVariableModifiers?.containsPrivateOrFileprivate() == true ? nil : objcAttribute } if let parentExtensionDecl = parent?.parent?.parent?.parent?.parent?.as(ExtensionDeclSyntax.self), - parentExtensionDecl.attributes.objCAttribute != nil { + parentExtensionDecl.attributes.objCAttribute != nil { return objcAttribute } return nil @@ -111,7 +111,7 @@ extension RedundantObjcAttributeRule { let nsCharSet = CharacterSet.whitespacesAndNewlines.bridge() let nsContent = file.contents.bridge() while nsCharSet - .characterIsMember(nsContent.character(at: violationRange.upperBound + whitespaceAndNewlineOffset)) { + .characterIsMember(nsContent.character(at: violationRange.upperBound + whitespaceAndNewlineOffset)) { whitespaceAndNewlineOffset += 1 } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift index dd91da179b..c6c3f6cb64 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift @@ -116,7 +116,7 @@ private extension ReturnClauseSyntax { return false } if let simpleReturnType = type.as(IdentifierTypeSyntax.self) { - return simpleReturnType.typeName == "Void" + return simpleReturnType.typeName == "Void" } if let tupleReturnType = type.as(TupleTypeSyntax.self) { return tupleReturnType.elements.isEmpty diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift index 7237a45514..13f605fda4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/TypeNameRule.swift @@ -90,7 +90,7 @@ private extension TypeNameRule { ) } if let caseCheckSeverity = nameConfiguration.validatesStartWithLowercase.severity, - name.first?.isLowercase == true { + name.first?.isLowercase == true { return ReasonedRuleViolation( position: identifier.positionAfterSkippingLeadingTrivia, reason: "Type name '\(name)' should start with an uppercase character", diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift index df9786f33d..1be10e9f3f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededBreakInSwitchRule.swift @@ -50,9 +50,9 @@ struct UnneededBreakInSwitchRule: Rule { ], corrections: [ embedInSwitch("something()\n ↓break") - : embedInSwitch("something()"), + : embedInSwitch("something()"), embedInSwitch("something()\n ↓break // line comment") - : embedInSwitch("something()\n // line comment"), + : embedInSwitch("something()\n // line comment"), embedInSwitch(""" something() ↓break @@ -60,14 +60,14 @@ struct UnneededBreakInSwitchRule: Rule { block comment */ """) - : embedInSwitch(""" + : embedInSwitch(""" something() /* block comment */ """), embedInSwitch("something()\n ↓break /// doc line comment") - : embedInSwitch("something()\n /// doc line comment"), + : embedInSwitch("something()\n /// doc line comment"), embedInSwitch(""" something() ↓break @@ -75,16 +75,16 @@ struct UnneededBreakInSwitchRule: Rule { /// doc block comment /// """) - : embedInSwitch(""" + : embedInSwitch(""" something() /// /// doc block comment /// """), embedInSwitch("something()\n ↓break", case: "default") - : embedInSwitch("something()", case: "default"), + : embedInSwitch("something()", case: "default"), embedInSwitch("something()\n ↓break", case: "case .foo, .foo2 where condition") - : embedInSwitch("something()", case: "case .foo, .foo2 where condition"), + : embedInSwitch("something()", case: "case .foo, .foo2 where condition"), ] ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift index da2c628ba9..60e6b62956 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnusedEnumeratedRule.swift @@ -243,9 +243,9 @@ private extension FunctionCallExprSyntax { var enumeratedPosition: AbsolutePosition? { if let memberAccess = calledExpression.as(MemberAccessExprSyntax.self), - memberAccess.base != nil, - memberAccess.declName.baseName.text == "enumerated", - hasNoArguments { + memberAccess.base != nil, + memberAccess.declName.baseName.text == "enumerated", + hasNoArguments { return memberAccess.declName.positionAfterSkippingLeadingTrivia } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift index 25f5dd1575..27f7c3ad57 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/VoidFunctionInTernaryConditionRule.swift @@ -234,9 +234,9 @@ private extension CodeBlockItemSyntax { } private extension FunctionSignatureSyntax { - var allowsImplicitReturns: Bool { - returnClause?.allowsImplicitReturns ?? false - } + var allowsImplicitReturns: Bool { + returnClause?.allowsImplicitReturns ?? false + } } private extension SubscriptDeclSyntax { diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift index ac41e13867..2a4903fe9e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/XCTSpecificMatcherRule.swift @@ -25,7 +25,7 @@ private extension XCTSpecificMatcherRule { reason: "Prefer the specific matcher '\(suggestion)' instead" )) } else if configuration.matchers.contains(.oneArgumentAsserts), - let suggestion = OneArgXCTAssert.violations(in: node) { + let suggestion = OneArgXCTAssert.violations(in: node) { violations.append(ReasonedRuleViolation( position: node.positionAfterSkippingLeadingTrivia, reason: "Prefer the specific matcher '\(suggestion)' instead" diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift index 9f816b007f..514b8b01fe 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift @@ -139,7 +139,7 @@ private extension FunctionCallExprSyntax { let modifierName = memberAccess.declName.baseName.text if funcCall.isDirectAccessibilityModifier(modifierName) || - funcCall.isContainerExemptingModifier(modifierName) { + funcCall.isContainerExemptingModifier(modifierName) { return true } } @@ -187,7 +187,7 @@ private extension FunctionCallExprSyntax { let modifierName = memberAccess.declName.baseName.text if funcCall.isDirectAccessibilityModifier(modifierName) || - funcCall.isContainerExemptingModifier(modifierName) { + funcCall.isContainerExemptingModifier(modifierName) { return true } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift index 8901f882e7..24355db396 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRuleExamples.swift @@ -374,7 +374,7 @@ internal struct AccessibilityLabelForImageRuleExamples { } } """), - // MARK: - SwiftSyntax Migration Detection Improvements + // MARK: - SwiftSyntax Migration Detection Improvements // These violations would have been missed by the SourceKit implementation // but are now correctly detected by SwiftSyntax Example(""" diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift index e4bbbcdba9..abe85e3d8e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityTraitForButtonRule.swift @@ -161,8 +161,8 @@ private struct AccessibilityButtonTraitDeterminator { // Stop if we reach a new View declaration or similar boundary if currentSyntaxNode.is(StructDeclSyntax.self) || - currentSyntaxNode.is(ClassDeclSyntax.self) || - currentSyntaxNode.is(EnumDeclSyntax.self) { + currentSyntaxNode.is(ClassDeclSyntax.self) || + currentSyntaxNode.is(EnumDeclSyntax.self) { break } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift index a461e9a0e1..28ac98b22d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/ArrayInitRule.swift @@ -131,7 +131,7 @@ private extension ClosureSignatureSyntax { return list.onlyElement?.name.text } if let clause = parameterClause?.as(ClosureParameterClauseSyntax.self), clause.parameters.count == 1, - clause.parameters.first?.secondName == nil { + clause.parameters.first?.secondName == nil { return clause.parameters.first?.firstName.text } return nil diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift index 0268d68813..579e2afc0f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AsyncWithoutAwaitRule.swift @@ -121,7 +121,7 @@ private extension AsyncWithoutAwaitRule { override func visitPost(_ node: VariableDeclSyntax) { if node.bindingSpecifier.tokenKind == .keyword(.let), - node.modifiers.contains(keyword: .async) { + node.modifiers.contains(keyword: .async) { functionScopes.modifyLast { $0.containsAwait = true } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift index 6df51d512a..ba6a1631bf 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/BlanketDisableCommandRule.swift @@ -61,7 +61,7 @@ struct BlanketDisableCommandRule: Rule, SourceKitFreeRule { """), Example("// swiftlint:disable all"), ].skipWrappingInCommentTests().skipDisableCommandTests() - ) + ) func validate(file: SwiftLintFile) -> [StyleViolation] { var violations: [StyleViolation] = [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift index 0227dcf8fe..b74f089317 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/CompilerProtocolInitRule.swift @@ -42,7 +42,7 @@ private extension CompilerProtocolInitRule { let argumentsNames = arguments.map(\.text) for compilerProtocol in ExpressibleByCompiler.allProtocols { guard compilerProtocol.initCallNames.contains(name), - compilerProtocol.match(arguments: argumentsNames) else { + compilerProtocol.match(arguments: argumentsNames) else { continue } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift index 45a18ed6a7..c392de8a49 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/DeploymentTargetRule.swift @@ -105,12 +105,12 @@ private extension DeploymentTargetRule { violationType: AvailabilityType) -> String? { guard let platform = DeploymentTargetConfiguration.Platform(rawValue: platform.text), let minVersion = platformToConfiguredMinVersion[platform.rawValue] else { - return nil + return nil } guard let version = try? Version(platform: platform, value: versionString), - version <= minVersion else { - return nil + version <= minVersion else { + return nil } return """ diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift index 2a8c744f1e..49ea44bb53 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/MissingDocsRule.swift @@ -220,9 +220,9 @@ private extension SyntaxProtocol { return false } let ifConfigDecl = itemList - .parent?.as(IfConfigClauseSyntax.self)? - .parent?.as(IfConfigClauseListSyntax.self)? - .parent?.as(IfConfigDeclSyntax.self) + .parent?.as(IfConfigClauseSyntax.self)? + .parent?.as(IfConfigClauseListSyntax.self)? + .parent?.as(IfConfigDeclSyntax.self) if let ifConfigDecl { return ifConfigDecl.hasDocComment } @@ -303,19 +303,19 @@ private extension Stack { func computeAcl(givenExplicitAcl acl: AccessControlLevel?, evalEffectiveAcl: Bool) -> AccessControlLevel { if let parentBehavior = peek() { switch parentBehavior { - case .local: - .private - case .actor, .class, .struct, .enum: - if let acl { - acl < parentBehavior.effectiveAcl || !evalEffectiveAcl ? acl : parentBehavior.effectiveAcl - } else { - parentBehavior.effectiveAcl >= .internal ? .internal : parentBehavior.effectiveAcl - } - case .protocol: - parentBehavior.effectiveAcl - case .extension: - acl ?? parentBehavior.effectiveAcl - } + case .local: + .private + case .actor, .class, .struct, .enum: + if let acl { + acl < parentBehavior.effectiveAcl || !evalEffectiveAcl ? acl : parentBehavior.effectiveAcl + } else { + parentBehavior.effectiveAcl >= .internal ? .internal : parentBehavior.effectiveAcl + } + case .protocol: + parentBehavior.effectiveAcl + case .extension: + acl ?? parentBehavior.effectiveAcl + } } else { acl ?? .internal } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift index 428b79721e..797f4f2c42 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift @@ -99,7 +99,7 @@ private func isOrphanedDocComment( while let (_, piece) = iterator.next() { switch piece { case .docLineComment, .docBlockComment, - .carriageReturns, .carriageReturnLineFeeds, .newlines, .spaces: + .carriageReturns, .carriageReturnLineFeeds, .newlines, .spaces: break case .lineComment, .blockComment: diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift index 440e9d9da0..9966914d1a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/PrivateSwiftUIStatePropertyRule.swift @@ -65,8 +65,8 @@ private extension PrivateSwiftUIStatePropertyRule { override func visitPost(_ node: VariableDeclSyntax) { guard node.parent?.is(MemberBlockItemSyntax.self) == true, - swiftUITypeScopes.peek() ?? false, - node.containsSwiftUIStateAccessLevelViolation + swiftUITypeScopes.peek() ?? false, + node.containsSwiftUIStateAccessLevelViolation else { return } @@ -102,8 +102,8 @@ private extension PrivateSwiftUIStatePropertyRule { override func visitPost(_ node: Syntax) { if node.is(ClassDeclSyntax.self) || - node.is(StructDeclSyntax.self) || - node.is(ActorDeclSyntax.self) { + node.is(StructDeclSyntax.self) || + node.is(ActorDeclSyntax.self) { swiftUITypeScopes.pop() } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift index 9b65c81127..5e39294c8a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/QuickDiscouragedCallRule.swift @@ -44,12 +44,12 @@ struct QuickDiscouragedCallRule: OptInRule { kind: SwiftExpressionKind, dictionary: SourceKittenDictionary) -> [StyleViolation] { // is it a call to a restricted method? - guard - kind == .call, - let name = dictionary.name, - let kindName = QuickCallKind(rawValue: name), - QuickCallKind.restrictiveKinds.contains(kindName) - else { return [] } + guard kind == .call, + let name = dictionary.name, + let kindName = QuickCallKind(rawValue: name), + QuickCallKind.restrictiveKinds.contains(kindName) else { + return [] + } return violationOffsets(in: dictionary.enclosedArguments).map { StyleViolation(ruleDescription: Self.description, @@ -73,29 +73,30 @@ struct QuickDiscouragedCallRule: OptInRule { } private func toViolationOffsets(dictionary: SourceKittenDictionary) -> [ByteCount] { - guard - dictionary.kind != nil, - let offset = dictionary.offset - else { return [] } + guard dictionary.kind != nil, + let offset = dictionary.offset else { + return [] + } if dictionary.expressionKind == .call, - let name = dictionary.name, QuickCallKind(rawValue: name) == nil { + let name = dictionary.name, QuickCallKind(rawValue: name) == nil { return [offset] } - guard dictionary.expressionKind != .call else { return [] } + guard dictionary.expressionKind != .call else { + return [] + } return dictionary.substructure.compactMap(toViolationOffset) } private func toViolationOffset(dictionary: SourceKittenDictionary) -> ByteCount? { - guard - let name = dictionary.name, - let offset = dictionary.offset, - dictionary.expressionKind == .call, - QuickCallKind(rawValue: name) == nil - else { return nil } - + guard let name = dictionary.name, + let offset = dictionary.offset, + dictionary.expressionKind == .call, + QuickCallKind(rawValue: name) == nil else { + return nil + } return offset } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift index 4ef5f9b115..c0749e278a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TodoRule.swift @@ -57,10 +57,10 @@ private extension TriviaPiece { for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] { switch self { case - .blockComment(let comment), - .lineComment(let comment), - .docBlockComment(let comment), - .docLineComment(let comment): + .blockComment(let comment), + .lineComment(let comment), + .docBlockComment(let comment), + .docLineComment(let comment): // Construct a regex string considering only keywords. let searchKeywords = todoKeywords.map(\.rawValue).joined(separator: "|") diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift index fb353a8ac6..db265b8a1f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/TypesafeArrayInitRule.swift @@ -52,14 +52,14 @@ struct TypesafeArrayInitRule: AnalyzerRule { private static let parentRule = ArrayInitRule() private static let mapTypePatterns = [ - regex(""" - \\Q \ - \\Q(Self) -> ((Self.Element) throws -> T) throws -> [T]\\E - """), - regex(""" - \\Q (Self) -> ((Self.Element) throws(E) -> T) throws(E) -> [T]\\E - """), + regex(""" + \\Q \ + \\Q(Self) -> ((Self.Element) throws -> T) throws -> [T]\\E + """), + regex(""" + \\Q (Self) -> ((Self.Element) throws(E) -> T) throws(E) -> [T]\\E + """), ] func validate(file: SwiftLintFile, compilerArguments: [String]) -> [StyleViolation] { diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift index c5a6b7fc7c..cc2ea6c5ad 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnneededOverrideRule.swift @@ -96,7 +96,7 @@ private extension OverridableDecl { } guard let call = extractFunctionCallSyntax(statement.item), - let member = call.calledExpression.as(MemberAccessExprSyntax.self), + let member = call.calledExpression.as(MemberAccessExprSyntax.self), member.base?.is(SuperExprSyntax.self) == true, member.declName.baseName.text == name else { return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift index cafd96f719..6faf97d0a2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedDeclarationRule.swift @@ -161,9 +161,9 @@ private extension SwiftLintFile { // Skip CodingKeys as they are used for Codable generation if kind == .enum, - indexEntity.name == "CodingKeys", - case let allRelatedUSRs = indexEntity.traverseEntitiesDepthFirst(traverseBlock: { $1.usr }), - allRelatedUSRs.contains("s:s9CodingKeyP") { + indexEntity.name == "CodingKeys", + case let allRelatedUSRs = indexEntity.traverseEntitiesDepthFirst(traverseBlock: { $1.usr }), + allRelatedUSRs.contains("s:s9CodingKeyP") { return nil } @@ -263,8 +263,8 @@ private extension SourceKittenDictionary { func propertyAtOffset(_ offset: ByteCount, property: KeyPath) -> T? { if let nameOffset, - nameOffset == offset, - let field = self[keyPath: property] { + nameOffset == offset, + let field = self[keyPath: property] { return field } for child in substructure { diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift index c6b47b9c4e..93273e627d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedImportRule.swift @@ -179,7 +179,7 @@ private extension SwiftLintFile { if nextIsModuleImport { nextIsModuleImport = false if let importedModule = cursorInfo.moduleName, - cursorInfo.kind == "source.lang.swift.ref.module" { + cursorInfo.kind == "source.lang.swift.ref.module" { imports.insert(importedModule) continue } @@ -207,7 +207,7 @@ private extension SwiftLintFile { // Operators are omitted in the editor.open request and thus have to be looked up by the indexsource request func operatorImports(arguments: [String], processedTokenOffsets: Set) -> Set { guard let index = (try? Request.index(file: path!, arguments: arguments).sendIfNotDisabled()) - .map(SourceKittenDictionary.init) else { + .map(SourceKittenDictionary.init) else { Issue.indexingError(path: path, ruleID: UnusedImportRule.identifier).print() return [] } @@ -230,7 +230,7 @@ private extension SwiftLintFile { file: path!, offset: ByteCount(offset), arguments: arguments ) guard let cursorInfo = (try? cursorInfoRequest.sendIfNotDisabled()) - .map(SourceKittenDictionary.init) else { + .map(SourceKittenDictionary.init) else { Issue.missingCursorInfo(path: path, ruleID: UnusedImportRule.identifier).print() continue } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift index a3073f47b5..0c75a4f8c6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedSetterValueRule.swift @@ -142,7 +142,7 @@ private extension UnusedSetterValueRule { let visitor = NewValueUsageVisitor(variableName: variableName) if !visitor.walk(tree: node, handler: \.isVariableUsed) { if Syntax(node).closestVariableOrSubscript()?.modifiers?.contains(keyword: .override) == true, - let body = node.body, body.statements.isEmpty { + let body = node.body, body.statements.isEmpty { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift index 709ec62e7e..5f0562130f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift @@ -74,8 +74,8 @@ private extension WeakDelegateRule { override func visitPost(_ node: VariableDeclSyntax) { guard node.hasDelegateSuffix, node.weakOrUnownedModifier == nil, - !node.hasComputedBody, - !node.containsIgnoredAttribute, + !node.hasComputedBody, + !node.containsIgnoredAttribute, let parent = node.parent, Syntax(parent).enclosingClass() != nil else { return diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift index 8c85e9f566..546620edc3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/YodaConditionRule.swift @@ -89,9 +89,9 @@ private extension YodaConditionRule { let lhs = children[lhsIdx] if lhs.isLiteral, children.startIndex == lhsIdx || children[children.index(before: lhsIdx)].isLogicalBinaryOperator { - // Literal is at the very beginning of the expression or the previous token is an operator with - // weaker binding. Thus, the literal is unique on the left-hand side of the comparison operator. - violations.append(lhs.positionAfterSkippingLeadingTrivia) + // Literal is at the very beginning of the expression or the previous token is an operator with + // weaker binding. Thus, the literal is unique on the left-hand side of the comparison operator. + violations.append(lhs.positionAfterSkippingLeadingTrivia) } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift index fa35cfface..199d018f2c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift @@ -147,7 +147,7 @@ private extension LineLengthRule { // Check if line starts with comment markers if lineContent.hasPrefix("//") || lineContent.hasPrefix("/*") || - (lineContent.hasPrefix("*/") && lineContent.count == 2) { + (lineContent.hasPrefix("*/") && lineContent.count == 2) { // Now verify using SwiftSyntax that this line doesn't contain any tokens var hasNonCommentContent = false @@ -225,7 +225,7 @@ private final class MultilineStringLiteralVisitor: SyntaxVisitor { override func visitPost(_ node: StringLiteralExprSyntax) { guard node.openingQuote.tokenKind == .multilineStringQuote || - (node.openingPounds != nil && node.openingQuote.tokenKind == .stringQuote) else { + (node.openingPounds != nil && node.openingQuote.tokenKind == .stringQuote) else { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index c9978a5343..6565ca0752 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -9,7 +9,7 @@ private func wrapExample( file: StaticString = #filePath, line: UInt = #line) -> Example { Example("\(prefix)\(type) Abc {\n" + - repeatElement(template, count: count).joined() + "\(add)}\n", file: file, line: line) + repeatElement(template, count: count).joined() + "\(add)}\n", file: file, line: line) } @SwiftSyntaxRule @@ -30,7 +30,7 @@ struct TypeBodyLengthRule: Rule { ] }), triggeringExamples: ["class", "struct", "enum", "actor"].map({ type in - wrapExample(prefix: "↓", type, "let abc = 0\n", 251) + wrapExample(prefix: "↓", type, "let abc = 0\n", 251) }) ) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift index e3ae47c1ca..824178a569 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/FirstWhereRule.swift @@ -56,8 +56,8 @@ private extension ExprSyntax { return true } if let functionCall = self.as(FunctionCallExprSyntax.self), - let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), - calledExpression.baseName.text == "NSPredicate" { + let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), + calledExpression.baseName.text == "NSPredicate" { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift index f053764d5e..b8dafbcef4 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/LastWhereRule.swift @@ -52,8 +52,8 @@ private extension ExprSyntax { return true } if let functionCall = self.as(FunctionCallExprSyntax.self), - let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), - calledExpression.baseName.text == "NSPredicate" { + let calledExpression = functionCall.calledExpression.as(DeclReferenceExprSyntax.self), + calledExpression.baseName.text == "NSPredicate" { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift index a179edf276..a4ba9e4898 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/ReduceIntoRule.swift @@ -152,8 +152,8 @@ private extension ExprSyntax { return identifierExpr.isCopyOnWriteType } if let memberAccesExpr = expr.calledExpression.as(MemberAccessExprSyntax.self), - memberAccesExpr.declName.baseName.text == "init", - let identifierExpr = memberAccesExpr.base?.identifierExpr { + memberAccesExpr.declName.baseName.text == "init", + let identifierExpr = memberAccesExpr.base?.identifierExpr { return identifierExpr.isCopyOnWriteType } if expr.calledExpression.isCopyOnWriteType { @@ -162,7 +162,7 @@ private extension ExprSyntax { } return false - } + } var identifierExpr: DeclReferenceExprSyntax? { if let identifierExpr = self.as(DeclReferenceExprSyntax.self) { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift index 90413da9b2..e164c5dc00 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift @@ -102,14 +102,14 @@ struct DeploymentTargetConfiguration: SeverityBasedRuleConfiguration { var parameterDescription: RuleConfigurationDescription? { let targets = Dictionary(uniqueKeysWithValues: [ - iOSDeploymentTarget, - iOSAppExtensionDeploymentTarget, - macOSDeploymentTarget, - macOSAppExtensionDeploymentTarget, - watchOSDeploymentTarget, - watchOSAppExtensionDeploymentTarget, - tvOSDeploymentTarget, - tvOSAppExtensionDeploymentTarget, + iOSDeploymentTarget, + iOSAppExtensionDeploymentTarget, + macOSDeploymentTarget, + macOSAppExtensionDeploymentTarget, + watchOSDeploymentTarget, + watchOSAppExtensionDeploymentTarget, + tvOSDeploymentTarget, + tvOSAppExtensionDeploymentTarget, ].map { ($0.platform.configurationKey, $0) }) severityConfiguration for (platform, target) in targets.sorted(by: { $0.key < $1.key }) { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift index aed18138ff..d58dce5cd3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/UnusedImportConfiguration.swift @@ -10,9 +10,9 @@ struct TransitiveModuleConfiguration: Equatable, AcceptableByConfi init(fromAny configuration: Any, context _: String) throws { guard let configurationDict = configuration as? [String: Any], - Set(configurationDict.keys) == ["module", "allowed_transitive_imports"], - let importedModule = configurationDict["module"] as? String, - let transitivelyImportedModules = configurationDict["allowed_transitive_imports"] as? [String] + Set(configurationDict.keys) == ["module", "allowed_transitive_imports"], + let importedModule = configurationDict["module"] as? String, + let transitivelyImportedModules = configurationDict["allowed_transitive_imports"] as? [String] else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift index eea922a5cf..7df46cf426 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureSpacingRule.swift @@ -136,7 +136,7 @@ private extension TokenSyntax { return true } if let previousToken = previousToken(viewMode: .sourceAccurate), - case .spaces(1) = Array(previousToken.trailingTrivia).last { + case .spaces(1) = Array(previousToken.trailingTrivia).last { return true } return false @@ -147,7 +147,7 @@ private extension TokenSyntax { return true } if let nextToken = nextToken(viewMode: .sourceAccurate), - case .spaces(1) = nextToken.leadingTrivia.first { + case .spaces(1) = nextToken.leadingTrivia.first { return true } return false @@ -197,7 +197,7 @@ private extension TokenSyntax { return true } if let nextToken = nextToken(viewMode: .sourceAccurate), - allowedKinds.contains(nextToken.tokenKind) { + allowedKinds.contains(nextToken.tokenKind) { return true } return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift index 893e912a3c..a4a995bfeb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CollectionAlignmentRule.swift @@ -26,7 +26,7 @@ private extension CollectionAlignmentRule { override func visitPost(_ node: DictionaryElementListSyntax) { let locations = node.map { element in let position = configuration.alignColons ? element.colon.positionAfterSkippingLeadingTrivia : - element.key.positionAfterSkippingLeadingTrivia + element.key.positionAfterSkippingLeadingTrivia let location = locationConverter.location(for: position) let graphemeColumn: Int diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift index 30af47edc1..03f5967b4f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CommaInheritanceRule.swift @@ -3,7 +3,7 @@ import SourceKittenFramework import SwiftSyntax struct CommaInheritanceRule: OptInRule, SubstitutionCorrectableRule, - SourceKitFreeRule { + SourceKitFreeRule { var configuration = SeverityConfiguration(.warning) static let description = RuleDescription( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift index 10dcabc645..c1635796f5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ContrastedOpeningBraceRule.swift @@ -80,7 +80,7 @@ private extension BracedSyntax { } if let closure = `as`(ClosureExprSyntax.self), closure.keyPathInParent == \FunctionCallExprSyntax.trailingClosure { - return closure.leftBrace.previousIndentationDecidingToken + return closure.leftBrace.previousIndentationDecidingToken } if let closureLabel = parent?.as(MultipleTrailingClosureElementSyntax.self)?.label { return closureLabel.previousIndentationDecidingToken diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift index 1b408096e4..6acd14aa85 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ControlStatementRule.swift @@ -175,7 +175,7 @@ private extension ExprSyntax { return nil } - private func containsTrailingClosure(_ node: Syntax) -> Bool { + private func containsTrailingClosure(_ node: Syntax) -> Bool { switch node.as(SyntaxEnum.self) { case .functionCallExpr(let node): node.trailingClosure != nil || node.calledExpression.is(ClosureExprSyntax.self) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift index 02e8e37cfa..a85c4a5a45 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ExplicitSelfRule.swift @@ -119,10 +119,10 @@ private extension StringView { func recursiveByteOffsets(_ dict: [String: Any]) -> [ByteCount] { let cur: [ByteCount] if let line = dict["key.line"] as? Int64, - let column = dict["key.column"] as? Int64, - let kindString = dict["key.kind"] as? String, - kindsToFind.contains(kindString), - let offset = byteOffset(forLine: line, bytePosition: column) { + let column = dict["key.column"] as? Int64, + let kindString = dict["key.kind"] as? String, + kindsToFind.contains(kindString), + let offset = byteOffset(forLine: line, bytePosition: column) { cur = [offset] } else { cur = [] diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift index a5e3cd3e8e..a269fde54f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LeadingWhitespaceRule.swift @@ -40,9 +40,9 @@ struct LeadingWhitespaceRule: CorrectableRule, SourceKitFreeRule { let whitespaceAndNewline = CharacterSet.whitespacesAndNewlines let spaceCount = file.contents.countOfLeadingCharacters(in: whitespaceAndNewline) guard spaceCount > 0, - let firstLineRange = file.lines.first?.range, - file.ruleEnabled(violatingRanges: [firstLineRange], for: self).isNotEmpty else { - return 0 + let firstLineRange = file.lines.first?.range, + file.ruleEnabled(violatingRanges: [firstLineRange], for: self).isNotEmpty else { + return 0 } let indexEnd = file.contents.index( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift index 4c78231ca5..842a6c42c3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/LiteralExpressionEndIndentationRule.swift @@ -220,17 +220,17 @@ extension LiteralExpressionEndIndentationRule { let contents = file.stringView guard elements.isNotEmpty, - let offset = dictionary.offset, - let length = dictionary.length, - let (startLine, _) = contents.lineAndCharacter(forByteOffset: offset), - let firstParamOffset = elements[0].offset, - let (firstParamLine, _) = contents.lineAndCharacter(forByteOffset: firstParamOffset), - startLine != firstParamLine, - let lastParamOffset = elements.last?.offset, - let (lastParamLine, _) = contents.lineAndCharacter(forByteOffset: lastParamOffset), - case let endOffset = offset + length - 1, - let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), - lastParamLine != endLine + let offset = dictionary.offset, + let length = dictionary.length, + let (startLine, _) = contents.lineAndCharacter(forByteOffset: offset), + let firstParamOffset = elements[0].offset, + let (firstParamLine, _) = contents.lineAndCharacter(forByteOffset: firstParamOffset), + startLine != firstParamLine, + let lastParamOffset = elements.last?.offset, + let (lastParamLine, _) = contents.lineAndCharacter(forByteOffset: lastParamOffset), + case let endOffset = offset + length - 1, + let (endLine, endPosition) = contents.lineAndCharacter(forByteOffset: endOffset), + lastParamLine != endLine else { return nil } @@ -239,8 +239,8 @@ extension LiteralExpressionEndIndentationRule { let regex = Self.notWhitespace let actual = endPosition - 1 guard let match = regex.firstMatch(in: file.contents, options: [], range: range)?.range, - case let expected = match.location - range.location, - expected != actual + case let expected = match.location - range.location, + expected != actual else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift index 2d048013ba..859368544b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineFunctionChainsRule.swift @@ -108,10 +108,9 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { let ranges = callRanges(file: file, kind: kind, dictionary: dictionary) let calls = ranges.compactMap { range -> (dotLine: Int, dotOffset: Int, range: ByteRange)? in - guard - let offset = callDotOffset(file: file, callRange: range), - let line = file.stringView.lineAndCharacter(forCharacterOffset: offset)?.line else { - return nil + guard let offset = callDotOffset(file: file, callRange: range), + let line = file.stringView.lineAndCharacter(forCharacterOffset: offset)?.line else { + return nil } return (dotLine: line, dotOffset: offset, range: range) } @@ -133,11 +132,10 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { private static let whitespaceDotRegex = regex("\\s*\\.") private func callDotOffset(file: SwiftLintFile, callRange: ByteRange) -> Int? { - guard - let range = file.stringView.byteRangeToNSRange(callRange), - case let regex = Self.whitespaceDotRegex, - let match = regex.matches(in: file.contents, options: [], range: range).last?.range else { - return nil + guard let range = file.stringView.byteRangeToNSRange(callRange), + case let regex = Self.whitespaceDotRegex, + let match = regex.matches(in: file.contents, options: [], range: range).last?.range else { + return nil } return match.location + match.length - 1 } @@ -145,11 +143,10 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { private static let newlineWhitespaceDotRegex = regex("\\n\\s*\\.") private func callHasLeadingNewline(file: SwiftLintFile, callRange: ByteRange) -> Bool { - guard - let range = file.stringView.byteRangeToNSRange(callRange), - case let regex = Self.newlineWhitespaceDotRegex, - regex.firstMatch(in: file.contents, options: [], range: range) != nil else { - return false + guard let range = file.stringView.byteRangeToNSRange(callRange), + case let regex = Self.newlineWhitespaceDotRegex, + regex.firstMatch(in: file.contents, options: [], range: range) != nil else { + return false } return true } @@ -189,17 +186,15 @@ struct MultilineFunctionChainsRule: ASTRule, OptInRule { call: SourceKittenDictionary, parentName: String, parentNameOffset: ByteCount) -> ByteRange? { - guard - case let contents = file.stringView, - let nameOffset = call.nameOffset, - parentNameOffset == nameOffset, - let nameLength = call.nameLength, - let bodyOffset = call.bodyOffset, - let bodyLength = call.bodyLength, - case let nameByteRange = ByteRange(location: nameOffset, length: nameLength), - let name = contents.substringWithByteRange(nameByteRange), - parentName.starts(with: name) - else { + guard case let contents = file.stringView, + let nameOffset = call.nameOffset, + parentNameOffset == nameOffset, + let nameLength = call.nameLength, + let bodyOffset = call.bodyOffset, + let bodyLength = call.bodyLength, + case let nameByteRange = ByteRange(location: nameOffset, length: nameLength), + let name = contents.substringWithByteRange(nameByteRange), + parentName.starts(with: name) else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift index 51c134d8db..0e360e122b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineLiteralBracketsRule.swift @@ -145,7 +145,7 @@ private extension MultilineLiteralBracketsRule { firstElement: (some ExprSyntaxProtocol)?, lastElement: (some ExprSyntaxProtocol)?) { guard let firstElement, let lastElement, - isMultiline(node) else { + isMultiline(node) else { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift index 5021c338c6..156d6824b6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift @@ -99,7 +99,7 @@ struct MultilineParametersBracketsRule: OptInRule { // find violations at current level if let kind = substructure.declarationKind, - SwiftDeclarationKind.functionKinds.contains(kind) { + SwiftDeclarationKind.functionKinds.contains(kind) { guard let nameOffset = substructure.nameOffset, let nameLength = substructure.nameLength, diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift index d23963467a..04e147d210 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift @@ -104,15 +104,15 @@ private extension NumberSeparatorValidator { func violation(token: TokenSyntax) -> NumberSeparatorViolation? { let content = token.text guard isDecimal(number: content), - !isInValidRanges(number: content) + !isInValidRanges(number: content) else { return nil } let exponential = CharacterSet(charactersIn: "eE") guard case let exponentialComponents = content.components(separatedBy: exponential), - let nonExponential = exponentialComponents.first else { - return nil + let nonExponential = exponentialComponents.first else { + return nil } let components = nonExponential.components(separatedBy: ".") @@ -124,8 +124,8 @@ private extension NumberSeparatorValidator { } guard let integerSubstring = components.first, - case let (valid, expected) = isValid(number: integerSubstring, isFraction: false), - !valid || !validFraction + case let (valid, expected) = isValid(number: integerSubstring, isFraction: false), + !valid || !validFraction else { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift index 7205157801..01715915ec 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OpeningBraceRule.swift @@ -29,7 +29,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ActorDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.actorKeyword) { return } @@ -38,7 +38,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ClassDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.classKeyword) { return } @@ -47,7 +47,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: EnumDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.enumKeyword) { return } @@ -56,7 +56,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ExtensionDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.extensionKeyword) { return } @@ -65,7 +65,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ProtocolDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.protocolKeyword) { return } @@ -74,7 +74,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: StructDeclSyntax) { if configuration.ignoreMultilineTypeHeaders, - hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) { + hasMultilinePredecessors(node.memberBlock, keyword: node.structKeyword) { return } @@ -85,7 +85,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: ForStmtSyntax) { if configuration.ignoreMultilineStatementConditions, - hasMultilinePredecessors(node.body, keyword: node.forKeyword) { + hasMultilinePredecessors(node.body, keyword: node.forKeyword) { return } @@ -94,7 +94,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: IfExprSyntax) { if configuration.ignoreMultilineStatementConditions, - hasMultilinePredecessors(node.body, keyword: node.ifKeyword) { + hasMultilinePredecessors(node.body, keyword: node.ifKeyword) { return } @@ -103,7 +103,7 @@ private extension OpeningBraceRule { override func visitPost(_ node: WhileStmtSyntax) { if configuration.ignoreMultilineStatementConditions, - hasMultilinePredecessors(node.body, keyword: node.whileKeyword) { + hasMultilinePredecessors(node.body, keyword: node.whileKeyword) { return } @@ -114,8 +114,8 @@ private extension OpeningBraceRule { override func visitPost(_ node: FunctionDeclSyntax) { if let body = node.body, - configuration.shouldIgnoreMultilineFunctionSignatures, - hasMultilinePredecessors(body, keyword: node.funcKeyword) { + configuration.shouldIgnoreMultilineFunctionSignatures, + hasMultilinePredecessors(body, keyword: node.funcKeyword) { return } @@ -124,8 +124,8 @@ private extension OpeningBraceRule { override func visitPost(_ node: InitializerDeclSyntax) { if let body = node.body, - configuration.shouldIgnoreMultilineFunctionSignatures, - hasMultilinePredecessors(body, keyword: node.initKeyword) { + configuration.shouldIgnoreMultilineFunctionSignatures, + hasMultilinePredecessors(body, keyword: node.initKeyword) { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift index 7e67a3c090..66a46c91da 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift @@ -68,9 +68,9 @@ struct OperatorUsageWhitespaceRule: OptInRule, CorrectableRule, SourceKitFreeRul let equalityOperatorRegex = regex("\\s+=\\s") guard let match = equalityOperatorRegex.firstMatch( - in: matchedString, - options: [], - range: matchedString.fullNSRange), + in: matchedString, + options: [], + range: matchedString.fullNSRange), match.range == matchedString.fullNSRange else { return false diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift index d07022130c..5f4993acab 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SelfBindingRule.swift @@ -91,8 +91,8 @@ private extension SelfBindingRule { return super.visit(node.with(\.pattern, newPattern)) } if node.initializer == nil, - identifierPattern.identifier.text == "self", - configuration.bindIdentifier != "self" { + identifierPattern.identifier.text == "self", + configuration.bindIdentifier != "self" { numberOfCorrections += 1 let newPattern = PatternSyntax( identifierPattern diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift index f7eeb9c310..9c194558f3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ShorthandArgumentRule.swift @@ -103,7 +103,7 @@ private extension ShorthandArgumentRule { if complexArguments.contains(argument) { nil } else if locationConverter.location(for: argument.position).line - <= startLine + configuration.allowUntilLineAfterOpeningBrace { + <= startLine + configuration.allowUntilLineAfterOpeningBrace { nil } else { ReasonedRuleViolation( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift index 705d8d4dec..94408f4709 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SortedImportsRule.swift @@ -84,8 +84,9 @@ struct SortedImportsRule: CorrectableRule, OptInRule { let contents = file.stringView let lines = file.lines let importLines: [Line] = importRanges.compactMap { range in - guard let line = contents.lineAndCharacter(forCharacterOffset: range.location)?.line - else { return nil } + guard let line = contents.lineAndCharacter(forCharacterOffset: range.location)?.line else { + return nil + } return lines[line - 1] } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift index 342037995b..9340ad6e0b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift @@ -184,7 +184,7 @@ private extension StatementPositionRule { let validator = Self.uncuddledMatchValidator(contents: file.stringView) let filterRanges = Self.uncuddledMatchFilter(contents: file.stringView, syntaxMap: syntaxMap) let validMatches = matches.compactMap(validator).filter(filterRanges) - .filter { file.ruleEnabled(violatingRanges: [$0.range], for: self).isNotEmpty } + .filter { file.ruleEnabled(violatingRanges: [$0.range], for: self).isNotEmpty } if validMatches.isEmpty { return 0 } @@ -195,7 +195,7 @@ private extension StatementPositionRule { var whitespace = contents.bridge().substring(with: range1) let newLines: String if newlineRange.location != NSNotFound { - newLines = contents.bridge().substring(with: newlineRange) + newLines = contents.bridge().substring(with: newlineRange) } else { newLines = "" } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift index fb68edaf03..91ebcec4b5 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingCommaRule.swift @@ -154,7 +154,7 @@ private extension TrailingCommaRule { .with(\.expression, lastElement.expression.with(\.trailingTrivia, [])) .with(\.trailingComma, .commaToken()) .with(\.trailingTrivia, lastElement.expression.trailingTrivia) - ) + ) return super.visit(newNode) case (_, true), (nil, false): return super.visit(node) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift index ff24517cdc..e7c144da0d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift @@ -49,7 +49,7 @@ private extension TrailingWhitespaceRule { // Apply `ignoresEmptyLines` configuration if configuration.ignoresEmptyLines && - line.trimmingCharacters(in: .whitespaces).isEmpty { + line.trimmingCharacters(in: .whitespaces).isEmpty { continue } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift index 081b73ed69..c7cd724404 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceBetweenCasesRule.swift @@ -148,7 +148,7 @@ struct VerticalWhitespaceBetweenCasesRule: Rule { let patternRegex = regex(pattern) let substring = file.contents.substring(from: range.location, length: range.length) guard let matchResult = patternRegex.firstMatch(in: substring, options: [], range: substring.fullNSRange), - matchResult.numberOfRanges > 1 else { + matchResult.numberOfRanges > 1 else { return false } diff --git a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift index ecd8a6d029..3f1ae318fd 100644 --- a/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Dictionary+SwiftLint.swift @@ -127,7 +127,7 @@ public struct SourceKittenDictionary { public var elements: [Self] { let elements = value["key.elements"] as? [any SourceKitRepresentable] ?? [] return elements.compactMap { $0 as? [String: any SourceKitRepresentable] } - .map(Self.init) + .map(Self.init) } public var entities: [Self] { diff --git a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift index b9ce4aba07..467723d54b 100644 --- a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift @@ -22,7 +22,7 @@ public extension Request { // Skip check for ConditionallySourceKitFree rules since we can't determine // at the type level if they're effectively SourceKit-free if ruleType is any SourceKitFreeRule.Type && - !(ruleType is any ConditionallySourceKitFree.Type) { + !(ruleType is any ConditionallySourceKitFree.Type) { queuedFatalError(""" '\(ruleID)' is a SourceKitFreeRule and should not be making requests to SourceKit. """) diff --git a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift index d89f5130fb..c8c48acac8 100644 --- a/Source/SwiftLintCore/Extensions/String+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/String+SwiftLint.swift @@ -55,8 +55,8 @@ public extension String { limitedBy: utf16.endIndex) ?? utf16.endIndex guard let fromIndex = Index(from16, within: self), - let toIndex = Index(to16, within: self) else { - return nil + let toIndex = Index(to16, within: self) else { + return nil } return fromIndex.. = [] for (ruleIdentifier, ruleViolations) in violationsByRuleIdentifier { - guard - let baselineViolations = baselineViolationsByRuleIdentifier[ruleIdentifier], - baselineViolations.isNotEmpty else { - filteredViolations.formUnion(ruleViolations) + guard let baselineViolations = baselineViolationsByRuleIdentifier[ruleIdentifier], + baselineViolations.isNotEmpty else { + filteredViolations.formUnion(ruleViolations) continue } diff --git a/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift b/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift index f0f28828e5..dc0360da38 100644 --- a/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift +++ b/Source/SwiftLintCore/Models/ChildOptionSeverityConfiguration.swift @@ -24,7 +24,7 @@ public struct ChildOptionSeverityConfiguration: RuleConfiguration, public mutating func apply(configuration: Any) throws { guard let configString = configuration as? String, - let optionSeverity = ChildOptionSeverity(rawValue: configString.lowercased()) else { + let optionSeverity = ChildOptionSeverity(rawValue: configString.lowercased()) else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } self.optionSeverity = optionSeverity diff --git a/Source/SwiftLintCore/Models/Command.swift b/Source/SwiftLintCore/Models/Command.swift index 09fb2db0bf..40a2e60f89 100644 --- a/Source/SwiftLintCore/Models/Command.swift +++ b/Source/SwiftLintCore/Models/Command.swift @@ -109,9 +109,9 @@ public struct Command: Equatable { // Store any text after the comment delimiter as the trailingComment. // The addition to currentIndex is to move past the delimiter trailingComment = String( - scanner - .string[scanner.currentIndex...] - .dropFirst(Self.commentDelimiter.count) + scanner + .string[scanner.currentIndex...] + .dropFirst(Self.commentDelimiter.count) ) } let ruleTexts = rawRuleTexts.components(separatedBy: .whitespacesAndNewlines).filter { diff --git a/Source/SwiftLintCore/Models/RuleDescription.swift b/Source/SwiftLintCore/Models/RuleDescription.swift index ae5f9565a5..7dc528f688 100644 --- a/Source/SwiftLintCore/Models/RuleDescription.swift +++ b/Source/SwiftLintCore/Models/RuleDescription.swift @@ -11,7 +11,7 @@ public struct RuleDescription: Equatable, Sendable { /// explanation of the rule's purpose and rationale. public let description: String - /// A longer explanation of the rule's purpose and rationale. Typically defined as a multiline string, long text + /// A longer explanation of the rule's purpose and rationale. Typically defined as a multiline string, long text /// lines should be wrapped. Markdown formatting is supported. Multiline code blocks will be formatted as /// `swift` code unless otherwise specified, and will automatically be indented by four spaces when printed /// to the console. diff --git a/Source/SwiftLintCore/Models/SwiftVersion.swift b/Source/SwiftLintCore/Models/SwiftVersion.swift index 70da9f5d19..193283bf3c 100644 --- a/Source/SwiftLintCore/Models/SwiftVersion.swift +++ b/Source/SwiftLintCore/Models/SwiftVersion.swift @@ -92,7 +92,7 @@ public extension SwiftVersion { try? Request.customRequest(request: params).sendIfNotDisabled() } if let result, - let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch { + let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch { return SwiftVersion(rawValue: "\(major).\(minor).\(patch)") } } diff --git a/Source/SwiftLintCore/Protocols/Rule.swift b/Source/SwiftLintCore/Protocols/Rule.swift index 98427b4fb7..c81f2282e7 100644 --- a/Source/SwiftLintCore/Protocols/Rule.swift +++ b/Source/SwiftLintCore/Protocols/Rule.swift @@ -28,7 +28,7 @@ public protocol Rule { /// Create a description of how this rule has been configured to run. /// /// - parameter exclusiveOptions: A set of options that should be excluded from the description. - /// + /// /// - returns: A description of the rule's configuration. func createConfigurationDescription(exclusiveOptions: Set) -> RuleConfigurationDescription @@ -83,17 +83,17 @@ public protocol Rule { /// - ruleID: The name of a rule as used in a disable command. /// /// - Returns: A boolean value indicating whether the violation can be disabled by the given ID. - func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool - - /// Checks if a the rule is enabled in a given region. A specific rule ID can be provided in case a rule supports - /// more than one identifier. - /// - /// - Parameters: - /// - region: The region to check. - /// - ruleID: Rule identifier deviating from the default rule's name. - /// - /// - Returns: A boolean value indicating whether the rule is enabled in the given region. - func isEnabled(in region: Region, for ruleID: String) -> Bool + func canBeDisabled(violation: StyleViolation, by ruleID: RuleIdentifier) -> Bool + + /// Checks if a the rule is enabled in a given region. A specific rule ID can be provided in case a rule supports + /// more than one identifier. + /// + /// - Parameters: + /// - region: The region to check. + /// - ruleID: Rule identifier deviating from the default rule's name. + /// + /// - Returns: A boolean value indicating whether the rule is enabled in the given region. + func isEnabled(in region: Region, for ruleID: String) -> Bool } public extension Rule { diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index dbb7d626a6..8f5590a41a 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -50,8 +50,8 @@ public struct RegexConfiguration: SeverityBasedRuleConfiguration, executionMode.rawValue, ] if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject), - let jsonString = String(data: jsonData, encoding: .utf8) { - return jsonString + let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString } queuedFatalError("Could not serialize regex configuration for cache") } diff --git a/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift b/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift index 71752dd3ba..cd0d2056d7 100644 --- a/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift +++ b/Source/SwiftLintCore/Visitors/CodeBlockVisitor.swift @@ -32,7 +32,7 @@ open class CodeBlockVisitor: ViolationsSyntaxV return } if parent.is(FunctionCallExprSyntax.self) || parent.is(MultipleTrailingClosureElementSyntax.self), - node.keyPathInParent != \FunctionCallExprSyntax.calledExpression { + node.keyPathInParent != \FunctionCallExprSyntax.calledExpression { // Trailing closure collectViolations(for: node) } diff --git a/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift index d9b1c05545..f33da18fcd 100644 --- a/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift +++ b/Source/SwiftLintFramework/CompilerArgumentsExtractor.swift @@ -66,8 +66,8 @@ extension Array where Element == String { let responseFile = String(arg.dropFirst()) return (try? String(contentsOf: URL(fileURLWithPath: responseFile, isDirectory: false))).flatMap { $0.trimmingCharacters(in: .newlines) - .components(separatedBy: "\n") - .expandingResponseFiles + .components(separatedBy: "\n") + .expandingResponseFiles } ?? [arg] } } diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index 8807996788..ac494f9b18 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -273,8 +273,8 @@ extension Configuration { queuedPrintError("\(options.capitalizedVerb) Swift files \(filesInfo)") } let excludeLintableFilesBy = options.useExcludingByPrefix - ? Configuration.ExcludeBy.prefix - : .paths(excludedPaths: excludedPaths()) + ? Configuration.ExcludeBy.prefix + : .paths(excludedPaths: excludedPaths()) return options.paths.flatMap { self.lintableFiles( inPath: $0, diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift index c8326c0776..33ae5e8bb9 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Parsing.swift @@ -171,8 +171,8 @@ extension Configuration { ) { for key in dict.keys where !validGlobalKeys.contains(key) { guard let identifier = ruleList.identifier(for: key), - let ruleType = ruleList.list[identifier] else { - continue + let ruleType = ruleList.list[identifier] else { + continue } switch rulesMode { diff --git a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index 9c80db6ab7..be16658172 100644 --- a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -131,7 +131,7 @@ package struct LintOrAnalyzeCommand { Could not change working directory to '\(workingDirectory)'. \ Make sure it exists and is accessible. """ - ) + ) } } try await Signposts.record(name: "LintOrAnalyzeCommand.run") { @@ -236,9 +236,8 @@ package struct LintOrAnalyzeCommand { return try Baseline(fromPath: baselinePath) } catch { Issue.baselineNotReadable(path: baselinePath).print() - if - (error as? CocoaError)?.code != CocoaError.fileReadNoSuchFile || - options.writeBaseline != options.baseline { + if (error as? CocoaError)?.code != CocoaError.fileReadNoSuchFile || + options.writeBaseline != options.baseline { throw error } } diff --git a/Source/SwiftLintFramework/LintableFilesVisitor.swift b/Source/SwiftLintFramework/LintableFilesVisitor.swift index d18ba025f7..2e812f0ce9 100644 --- a/Source/SwiftLintFramework/LintableFilesVisitor.swift +++ b/Source/SwiftLintFramework/LintableFilesVisitor.swift @@ -142,7 +142,7 @@ struct LintableFilesVisitor { private static func loadLogCompilerInvocations(_ path: String) -> [[String]]? { if let data = FileManager.default.contents(atPath: path), - let logContents = String(data: data, encoding: .utf8) { + let logContents = String(data: data, encoding: .utf8) { if logContents.isEmpty { return nil } @@ -164,7 +164,7 @@ struct LintableFilesVisitor { } guard let object = try? JSONSerialization.jsonObject(with: fileContents), - let compileDB = object as? [[String: Any]] else { + let compileDB = object as? [[String: Any]] else { throw CompileCommandsLoadError.malformedCommands(path) } diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index b61b88fc18..d25adf42a5 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -373,7 +373,7 @@ public struct CollectedLinter { private func cachedStyleViolations(benchmark: Bool = false) -> ([StyleViolation], [(id: String, time: Double)])? { let start = Date() guard let cache, let file = file.path, - let cachedViolations = cache.violations(forFile: file, configuration: configuration) else { + let cachedViolations = cache.violations(forFile: file, configuration: configuration) else { return nil } diff --git a/Source/SwiftLintFramework/Models/LinterCache.swift b/Source/SwiftLintFramework/Models/LinterCache.swift index 75a4097216..c6c541255a 100644 --- a/Source/SwiftLintFramework/Models/LinterCache.swift +++ b/Source/SwiftLintFramework/Models/LinterCache.swift @@ -74,9 +74,9 @@ public final class LinterCache { internal func violations(forFile file: String, configuration: Configuration) -> [StyleViolation]? { guard let lastModification = fileManager.modificationDate(forFileAtPath: file), - let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file], - entry.lastModification == lastModification, - entry.swiftVersion == swiftVersion + let entry = fileCache(cacheDescription: configuration.cacheDescription).entries[file], + entry.lastModification == lastModification, + entry.swiftVersion == swiftVersion else { return nil } diff --git a/Tests/FrameworkTests/CustomRulesTests.swift b/Tests/FrameworkTests/CustomRulesTests.swift index 98b67a083c..cf98a15860 100644 --- a/Tests/FrameworkTests/CustomRulesTests.swift +++ b/Tests/FrameworkTests/CustomRulesTests.swift @@ -384,7 +384,7 @@ final class CustomRulesTests: SwiftLintTestCase { ] let example = Example( - """ + """ // swiftlint:disable custom1 custom3 return 10 """ diff --git a/Tests/FrameworkTests/GlobTests.swift b/Tests/FrameworkTests/GlobTests.swift index f4ed2f38b6..9ac90d424e 100644 --- a/Tests/FrameworkTests/GlobTests.swift +++ b/Tests/FrameworkTests/GlobTests.swift @@ -38,7 +38,7 @@ final class GlobTests: SwiftLintTestCase { func testNoMatchOneCharacterInBracket() { let files = Glob.resolveGlob(mockPath.stringByAppendingPathComponent("Level[ab].swift")) - XCTAssertTrue(files.isEmpty) + XCTAssertTrue(files.isEmpty) } func testMatchesCharacterInRange() { diff --git a/Tests/TestHelpers/TestHelpers.swift b/Tests/TestHelpers/TestHelpers.swift index bc4cc1ca07..79fc255681 100644 --- a/Tests/TestHelpers/TestHelpers.swift +++ b/Tests/TestHelpers/TestHelpers.swift @@ -47,10 +47,10 @@ public let allRuleIdentifiers = Set(RuleRegistry.shared.list.list.keys) public extension Configuration { func applyingConfiguration(from example: Example) -> Configuration { guard let exampleConfiguration = example.configuration, - case let .onlyConfiguration(onlyRules) = self.rulesMode, - let firstRule = (onlyRules.first { $0 != "superfluous_disable_command" }), - case let configDict: [_: any Sendable] = ["only_rules": onlyRules, firstRule: exampleConfiguration], - let typedConfiguration = try? Configuration(dict: configDict) else { return self } + case let .onlyConfiguration(onlyRules) = self.rulesMode, + let firstRule = (onlyRules.first { $0 != "superfluous_disable_command" }), + case let configDict: [_: any Sendable] = ["only_rules": onlyRules, firstRule: exampleConfiguration], + let typedConfiguration = try? Configuration(dict: configDict) else { return self } return merged(withChild: typedConfiguration, rootDirectory: rootDirectory) } } @@ -156,7 +156,7 @@ private func render(violations: [StyleViolation], in contents: String) -> String var contents = StringView(contents).lines.map(\.content) for violation in violations.sorted(by: { $0.location > $1.location }) { guard let line = violation.location.line, - let character = violation.location.character else { continue } + let character = violation.location.character else { continue } let message = String(repeating: " ", count: character - 1) + "^ " + [ "\(violation.severity.rawValue): ", @@ -267,10 +267,10 @@ private func testCorrection(_ correction: (Example, Example), #endif var config = configuration if let correctionConfiguration = correction.0.configuration, - case let .onlyConfiguration(onlyRules) = configuration.rulesMode, - let ruleToConfigure = (onlyRules.first { $0 != SuperfluousDisableCommandRule.identifier }), - case let configDict: [_: any Sendable] = ["only_rules": onlyRules, ruleToConfigure: correctionConfiguration], - let typedConfiguration = try? Configuration(dict: configDict) { + case let .onlyConfiguration(onlyRules) = configuration.rulesMode, + let ruleToConfigure = (onlyRules.first { $0 != SuperfluousDisableCommandRule.identifier }), + case let configDict: [_: any Sendable] = ["only_rules": onlyRules, ruleToConfigure: correctionConfiguration], + let typedConfiguration = try? Configuration(dict: configDict) { config = configuration.merged(withChild: typedConfiguration, rootDirectory: configuration.rootDirectory) } @@ -307,11 +307,11 @@ public extension XCTestCase { } guard let config = makeConfig( - ruleConfiguration, - ruleDescription.identifier, - skipDisableCommandTests: skipDisableCommandTests) else { - XCTFail("Failed to create configuration", file: (file), line: line) - return + ruleConfiguration, + ruleDescription.identifier, + skipDisableCommandTests: skipDisableCommandTests) else { + XCTFail("Failed to create configuration", file: (file), line: line) + return } let disableCommands: [String] From c97cf24797f2658938f8e4bac12cf94e5fe12b78 Mon Sep 17 00:00:00 2001 From: Chris Brakebill Date: Wed, 9 Jul 2025 15:09:38 -0400 Subject: [PATCH 58/80] Add `ignore_codingkeys` parameter in `nesting` rule (#5650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Mösch --- CHANGELOG.md | 5 ++ .../Rules/Metrics/NestingRule.swift | 10 +++- .../Rules/Metrics/NestingRuleExamples.swift | 56 +++++++++++++++++++ .../NestingConfiguration.swift | 2 + .../Extensions/SwiftSyntax+SwiftLint.swift | 15 +++++ .../default_rule_configurations.yml | 1 + 6 files changed, 88 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2f4a0f4d2..7c81bcd1ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,11 @@ `max_number_of_single_line_parameters` and detect mixed formatting. [GandaLF2006](https://github.com/GandaLF2006) +* Add `ignore_coding_keys` parameter to `nesting` rule. Setting this to true prevents + `CodingKey` enums from violating the rule. + [braker1nine](https://github.com/braker1nine) + [#5641](https://github.com/realm/SwiftLint/issues/5641) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift index 7eb1af8163..fbd4a49f86 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift @@ -64,7 +64,14 @@ private extension NestingRule { } override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { - validate(forFunction: false, triggeringToken: node.enumKeyword) + // if current defines coding keys and we're ignoring coding keys, then skip nesting rule + // push another level on and proceed to visit children + if configuration.ignoreCodingKeys && node.definesCodingKeys { + levels.push(levels.lastIsFunction) + } else { + validate(forFunction: false, triggeringToken: node.enumKeyword) + } + return .visitChildren } @@ -152,6 +159,7 @@ private extension NestingRule { if configuration.alwaysAllowOneTypeInFunctions && inFunction && !forFunction { return } + guard let severity = configuration.severity(with: targetLevel, for: level) else { return } let targetName = forFunction ? "Functions" : "Types" diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift index d94c196068..17c2122514 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRuleExamples.swift @@ -8,6 +8,7 @@ internal struct NestingRuleExamples { + nonTriggeringClosureAndStatementExamples + nonTriggeringProtocolExamples + nonTriggeringMixedExamples + + nonTriggeringExamplesIgnoreCodingKeys private static let nonTriggeringTypeExamples = detectingTypes.flatMap { type -> [Example] in @@ -228,6 +229,21 @@ internal struct NestingRuleExamples { """), ] } + + private static let nonTriggeringExamplesIgnoreCodingKeys: [Example] = [ + Example( + """ + struct Outer { + struct Inner { + enum CodingKeys: String, CodingKey { + case id + } + } + } + """, + configuration: ["ignore_coding_keys": true] + ), + ] } extension NestingRuleExamples { @@ -236,6 +252,8 @@ extension NestingRuleExamples { + triggeringClosureAndStatementExamples + triggeringProtocolExamples + triggeringMixedExamples + + triggeringExamplesCodingKeys + + triggeringExamplesIgnoreCodingKeys private static let triggeringTypeExamples = detectingTypes.flatMap { type -> [Example] in @@ -499,4 +517,42 @@ extension NestingRuleExamples { """), ] } + + private static let triggeringExamplesCodingKeys: [Example] = [ + Example(""" + struct Outer { + struct Inner { + ↓enum CodingKeys: String, CodingKey { + case id + } + } + } + """), + ] + + private static let triggeringExamplesIgnoreCodingKeys: [Example] = [ + Example( + """ + struct Outer { + struct Inner { + ↓enum Example: String, CodingKey { + case id + } + } + } + """, + configuration: ["ignore_coding_keys": true] + ), + Example( + """ + struct Outer { + enum CodingKeys: String, CodingKey { + case id + ↓struct S {} + } + } + """, + configuration: ["ignore_coding_keys": true] + ), + ] } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift index 750b1793ef..2600c45ab6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NestingConfiguration.swift @@ -15,6 +15,8 @@ struct NestingConfiguration: RuleConfiguration { private(set) var alwaysAllowOneTypeInFunctions = false @ConfigurationElement(key: "ignore_typealiases_and_associatedtypes") private(set) var ignoreTypealiasesAndAssociatedtypes = false + @ConfigurationElement(key: "ignore_coding_keys") + private(set) var ignoreCodingKeys = false func severity(with config: Severity, for level: Int) -> ViolationSeverity? { if let error = config.error, level > error { diff --git a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift index 77f960a349..b0e837fb63 100644 --- a/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/SwiftSyntax+SwiftLint.swift @@ -1,6 +1,8 @@ import SourceKittenFramework import SwiftSyntax +// swiftlint:disable file_length + // workaround for https://bugs.swift.org/browse/SR-10121 so we can use `Self` in a closure public protocol SwiftLintSyntaxVisitor: SyntaxVisitor {} extension SyntaxVisitor: SwiftLintSyntaxVisitor {} @@ -191,6 +193,19 @@ public extension EnumDeclSyntax { return rawValueTypes.contains(identifier) } } + + /// True if this enum is a `CodingKey`. For that, it has to be named `CodingKeys` + /// and must conform to the `CodingKey` protocol. + var definesCodingKeys: Bool { + guard let inheritedTypeCollection = inheritanceClause?.inheritedTypes, + name.text == "CodingKeys" else { + return false + } + + return inheritedTypeCollection.contains { element in + element.type.as(IdentifierTypeSyntax.self)?.name.text == "CodingKey" + } + } } public extension FunctionDeclSyntax { diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index b3171d833a..7bc328d8ca 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -693,6 +693,7 @@ nesting: check_nesting_in_closures_and_statements: true always_allow_one_type_in_functions: false ignore_typealiases_and_associatedtypes: false + ignore_coding_keys: false meta: opt-in: false correctable: false From 6af2aed49d943450f5e9c91c53b7f0ba019db80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 9 Jul 2025 21:54:59 +0200 Subject: [PATCH 59/80] Ensure that `closure_end_indentation` doesn't drop comments (#6158) --- .../Style/ClosureEndIndentationRule.swift | 51 ++++++++++--------- .../ClosureEndIndentationRuleExamples.swift | 22 ++++++-- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift index 0633aaee38..9c87e4e9f6 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRule.swift @@ -48,28 +48,30 @@ private extension ClosureEndIndentationRule { let actualIndentationColumn = rightBraceLocation.column - 1 if actualIndentationColumn != expectedIndentationColumn { - let correctionStart: AbsolutePosition - let correctionReplacement: String - - // Check if there's leading trivia on the right brace that contains a newline - let hasNewlineInTrivia = node.rightBrace.leadingTrivia.contains(where: \.isNewline) - - if hasNewlineInTrivia { - // If there's already a newline, we just need to fix the indentation. - // The range to replace is the trivia before the brace. - correctionStart = node.rightBrace.position - correctionReplacement = "\n" + String(repeating: " ", count: max(0, expectedIndentationColumn)) - } else if let previousToken = node.rightBrace.previousToken(viewMode: .sourceAccurate) { - // If no newline, we need to add one. The replacement will be inserted - // after the previous token and before the closing brace. - correctionStart = previousToken.endPositionBeforeTrailingTrivia - correctionReplacement = "\n" + String(repeating: " ", count: max(0, expectedIndentationColumn)) - } else { - // Fallback: If there's no previous token, which is unlikely for a closure body, - // replace the trivia before the brace. - correctionStart = node.rightBrace.position - correctionReplacement = "\n" + String(repeating: " ", count: max(0, expectedIndentationColumn)) - } + // Check if there's leading trivia on the right brace that ends with a newline and only whitespace + // after it. + let leadingTriviaEndsWithNewline = node.rightBrace.leadingTrivia + .reversed() + .drop(while: \.isSpaceOrTab) + .first + .map(\.isNewline) ?? false + + let (correctionStartPosition, correctionPartBeforeIndentation) = + if leadingTriviaEndsWithNewline { + // If there's already a newline, we just need to fix the indentation. + // The range to replace is the trivia before the brace. + ( + locationConverter.position(ofLine: rightBraceLocation.line, column: 1), + "" + ) + } else { + // If no newline, we need to add one. The replacement will be inserted + // after the previous token and before the closing brace. + ( + node.rightBrace.positionAfterSkippingLeadingTrivia, + "\n" + ) + } let reason = "expected \(expectedIndentationColumn), got \(actualIndentationColumn)" violations.append( @@ -78,9 +80,10 @@ private extension ClosureEndIndentationRule { reason: reason, severity: configuration.severity, correction: .init( - start: correctionStart, + start: correctionStartPosition, end: node.rightBrace.positionAfterSkippingLeadingTrivia, - replacement: correctionReplacement + replacement: correctionPartBeforeIndentation + + String(repeating: " ", count: max(0, expectedIndentationColumn)) ) ) ) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift index 29ade3afdf..56484d83eb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ClosureEndIndentationRuleExamples.swift @@ -139,7 +139,7 @@ internal struct ClosureEndIndentationRuleExamples { """): Example(""" function( closure: { x in - print(x) + print(x) \("") }) """), Example(""" @@ -172,18 +172,21 @@ internal struct ClosureEndIndentationRuleExamples { Example(""" function( closure: { x in - print(x) + print(x) // comment + // comment ↓ }, anotherClosure: { y in print(y) - }) + /* comment */}) """): Example(""" function( closure: { x in - print(x) + print(x) // comment + // comment }, anotherClosure: { y in print(y) + /* comment */ }) """), Example(""" @@ -214,7 +217,7 @@ internal struct ClosureEndIndentationRuleExamples { """): Example(""" function( closure: { x in - print(x) + print(x) \("") }, anotherClosure: { y in print(y) @@ -235,5 +238,14 @@ internal struct ClosureEndIndentationRuleExamples { print(y) }) """), + Example(""" + f { + // do something + ↓} + """): Example(""" + f { + // do something + } + """), ] } From df964661638ab1442bdcbb325c9eb7da98ee2e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 9 Jul 2025 22:33:49 +0200 Subject: [PATCH 60/80] Support deinitializers and subscripts in function body length checking (#6142) --- CHANGELOG.md | 3 + .../Metrics/FunctionBodyLengthRule.swift | 131 +++++++++++++++++- .../FunctionBodyLengthRuleTests.swift | 107 +++++--------- 3 files changed, 169 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c81bcd1ff..70ee743219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,9 @@ [braker1nine](https://github.com/braker1nine) [#5641](https://github.com/realm/SwiftLint/issues/5641) +* Support deinitializers and subscripts in `function_body_length` rule. + [SimplyDanny](https://github.com/SimplyDanny) + ### Bug Fixes * Improved error reporting when SwiftLint exits, because of an invalid configuration file diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index e5df6e56df..60930c44f8 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -4,16 +4,118 @@ import SwiftSyntax struct FunctionBodyLengthRule: Rule { var configuration = SeverityLevelsConfiguration(warning: 50, error: 100) + private static let testConfig = ["warning": 2] + static let description = RuleDescription( identifier: "function_body_length", name: "Function Body Length", description: "Function bodies should not span too many lines", - kind: .metrics + kind: .metrics, + nonTriggeringExamples: [ + Example("func f() {}", configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + let y = 1 + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + // comments + // will + // be + // ignored + } + """, configuration: Self.testConfig), + Example(""" + func f() { + let x = 0 + // empty lines will be ignored + + + } + """, configuration: Self.testConfig), + ], + + triggeringExamples: [ + Example(""" + ↓func f() { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓deinit { + let x = 0 + let y = 1 + let z = 2 + } + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓init() { + let x = 0 + let y = 1 + let z = 2 + } + } + """, configuration: Self.testConfig), + Example(""" + class C { + ↓subscript() -> Int { + let x = 0 + let y = 1 + return x + y + } + } + """, configuration: Self.testConfig), + Example(""" + struct S { + subscript() -> Int { + ↓get { + let x = 0 + let y = 1 + return x + y + } + ↓set { + let x = 0 + let y = 1 + let z = 2 + } + ↓willSet { + let x = 0 + let y = 1 + let z = 2 + } + } + } + """, configuration: Self.testConfig), + ] ) } private extension FunctionBodyLengthRule { final class Visitor: BodyLengthVisitor { + override func visitPost(_ node: DeinitializerDeclSyntax) { + if let body = node.body { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.deinitKeyword, + objectName: "Deinitializer" + ) + } + } + override func visitPost(_ node: FunctionDeclSyntax) { if let body = node.body { registerViolations( @@ -35,5 +137,32 @@ private extension FunctionBodyLengthRule { ) } } + + override func visitPost(_ node: SubscriptDeclSyntax) { + guard let body = node.accessorBlock else { + return + } + if case .getter = body.accessors { + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: node.subscriptKeyword, + objectName: "Subscript" + ) + } + if case let .accessors(accessors) = body.accessors { + for accessor in accessors { + guard let body = accessor.body else { + continue + } + registerViolations( + leftBrace: body.leftBrace, + rightBrace: body.rightBrace, + violationNode: accessor.accessorSpecifier, + objectName: "Accessor" + ) + } + } + } } } diff --git a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift index ba6601d8f1..41422a8dd1 100644 --- a/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift +++ b/Tests/BuiltInRulesTests/FunctionBodyLengthRuleTests.swift @@ -2,102 +2,67 @@ import TestHelpers import XCTest -private func funcWithBody(_ body: String, - violates: Bool = false, - file: StaticString = #filePath, - line: UInt = #line) -> Example { - let marker = violates ? "↓" : "" - return Example("func \(marker)abc() {\n\(body)}\n", file: file, line: line) -} - -private func violatingFuncWithBody(_ body: String, file: StaticString = #filePath, line: UInt = #line) -> Example { - funcWithBody(body, violates: true, file: file, line: line) -} - final class FunctionBodyLengthRuleTests: SwiftLintTestCase { - func testFunctionBodyLengths() { - let longFunctionBody = funcWithBody(repeatElement("x = 0\n", count: 49).joined()) - XCTAssertEqual(self.violations(longFunctionBody), []) + func testWarning() { + let example = Example(""" + func f() { + let x = 0 + let y = 1 + let z = 2 + } + """) - let longerFunctionBody = violatingFuncWithBody(repeatElement("x = 0\n", count: 51).joined()) XCTAssertEqual( - self.violations(longerFunctionBody), + self.violations(example, configuration: ["warning": 2, "error": 4]), [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, + severity: .warning, location: Location(file: nil, line: 1, character: 1), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines" + reason: """ + Function body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ ), ] ) - - let longerFunctionBodyWithEmptyLines = funcWithBody( - repeatElement("\n", count: 100).joined() - ) - XCTAssertEqual(self.violations(longerFunctionBodyWithEmptyLines), []) } - func testFunctionBodyLengthsWithComments() { - let longFunctionBodyWithComments = funcWithBody( - repeatElement("x = 0\n", count: 49).joined() + - "// comment only line should be ignored.\n" - ) - XCTAssertEqual(violations(longFunctionBodyWithComments), []) + func testError() { + let example = Example(""" + func f() { + let x = 0 + let y = 1 + let z = 2 + } + """) - let longerFunctionBodyWithComments = violatingFuncWithBody( - repeatElement("x = 0\n", count: 51).joined() + - "// comment only line should be ignored.\n" - ) XCTAssertEqual( - self.violations(longerFunctionBodyWithComments), + self.violations(example, configuration: ["warning": 1, "error": 2]), [ StyleViolation( ruleDescription: FunctionBodyLengthRule.description, + severity: .error, location: Location(file: nil, line: 1, character: 1), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines" + reason: """ + Function body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ ), ] ) } - func testFunctionBodyLengthsWithMultilineComments() { - let longFunctionBodyWithMultilineComments = funcWithBody( - repeatElement("x = 0\n", count: 49).joined() + - "/* multi line comment only line should be ignored.\n*/\n" - ) - XCTAssertEqual(self.violations(longFunctionBodyWithMultilineComments), []) + func testViolationMessages() { + let types = FunctionBodyLengthRule.description.triggeringExamples.flatMap { + self.violations($0, configuration: ["warning": 2]) + }.compactMap { + $0.reason.split(separator: " ", maxSplits: 1).first + } - let longerFunctionBodyWithMultilineComments = violatingFuncWithBody( - repeatElement("x = 0\n", count: 51).joined() + - "/* multi line comment only line should be ignored.\n*/\n" - ) - XCTAssertEqual( - self.violations(longerFunctionBodyWithMultilineComments), - [ - StyleViolation( - ruleDescription: FunctionBodyLengthRule.description, - location: Location(file: nil, line: 1, character: 1), - reason: "Function body should span 50 lines or less excluding comments and " + - "whitespace: currently spans 51 lines" - ), - ] - ) - } - - func testConfiguration() { - let function = violatingFuncWithBody(repeatElement("x = 0\n", count: 10).joined()) - - XCTAssertEqual(self.violations(function, configuration: ["warning": 12]).count, 0) - XCTAssertEqual(self.violations(function, configuration: ["warning": 12, "error": 14]).count, 0) - XCTAssertEqual( - self.violations(function, configuration: ["warning": 8]).map(\.reason), - ["Function body should span 8 lines or less excluding comments and whitespace: currently spans 10 lines"] - ) XCTAssertEqual( - self.violations(function, configuration: ["warning": 12, "error": 8]).map(\.reason), - ["Function body should span 8 lines or less excluding comments and whitespace: currently spans 10 lines"] + types, + ["Function", "Deinitializer", "Initializer", "Subscript", "Accessor", "Accessor", "Accessor"] ) } From 571e6c18189a95abc515f49cc7a2692a4df6836c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 9 Jul 2025 22:43:50 +0200 Subject: [PATCH 61/80] Run remaining Azure build jobs on GitHub Actions (#6149) --- .github/actions/bazel-linux-build/action.yml | 6 +- .github/workflows/docs.yml | 61 ++++++++++++++++++++ .github/workflows/release.yml | 1 + .github/workflows/test.yml | 24 +++++++- .gitignore | 1 + Makefile | 6 ++ SwiftLint.podspec | 2 +- azure-pipelines.yml | 55 ------------------ tools/push-docs | 25 -------- 9 files changed, 98 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 azure-pipelines.yml delete mode 100755 tools/push-docs diff --git a/.github/actions/bazel-linux-build/action.yml b/.github/actions/bazel-linux-build/action.yml index 40293a7ced..685acc51c4 100644 --- a/.github/actions/bazel-linux-build/action.yml +++ b/.github/actions/bazel-linux-build/action.yml @@ -1,5 +1,9 @@ name: Bazel Linux Build description: Common steps to build SwiftLint with Bazel on GitHub Linux runners +inputs: + target: + description: The Bazel target to build + default: //:swiftlint runs: using: composite steps: @@ -15,6 +19,6 @@ runs: shell: bash run: | export PATH="/usr/share/swift/usr/bin:$PATH" - bazel build --config release //:swiftlint + bazel build --config release ${{ inputs.target }} env: CC: clang diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..cb12d47d0d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,61 @@ +name: Documentation + +on: + push: + branches: [main] + pull_request: + +jobs: + create-docs: + name: Create + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Build SwiftLint and SourceKitten + uses: ./.github/actions/bazel-linux-build + with: + target: "//:swiftlint @com_github_jpsim_sourcekitten//:sourcekitten" + env: + CI_BAZELRC_FILE_CONTENT: ${{ secrets.CI_BAZELRC_FILE_CONTENT }} + - uses: actions/cache@v4 + with: + key: ${{ runner.os }}-swift-spm-${{ hashFiles('Package.resolved') }} + restore-keys: ${{ runner.os }}-swift-spm- + path: .build + - name: Generate documentation + run: | + export PATH="/usr/share/swift/usr/bin:$PATH" + make docs_linux + - name: Validate documentation coverage + run: | + if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then + echo "Undocumented declarations:" + cat docs/undocumented.json + exit 1 + fi + - name: Upload documentation + if: github.event_name == 'push' + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + deploy-docs: + name: Deploy + runs-on: ubuntu-24.04 + needs: create-docs + if: github.event_name == 'push' + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy documentation + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bddfa6173e..06cd5b851a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,7 @@ jobs: run: | sed 's/__VERSION__/${{ inputs.version }}/g' tools/Version.swift.template > Source/SwiftLintFramework/Models/Version.swift sed -i -e '3s/.*/ version = "${{ inputs.version }}",/' MODULE.bazel + sed -i -e "s/^\(\s*s\.version\s*=\s*'\)[^']*'/\1${{ inputs.version }}'/" SwiftLint.podspec - name: Configure Git author uses: Homebrew/actions/git-user-config@master with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 37543dc15e..eb26266e80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,29 @@ jobs: key: ${{ runner.os }}-swift-spm-${{ hashFiles('Package.resolved') }} restore-keys: ${{ runner.os }}-swift-spm- path: .build - - name: Run SPM tests + - name: Run tests uses: ./.github/actions/run-make with: rule: spm_test + + spm_macos: + name: SPM, macOS ${{ matrix.macOS }}, Xcode ${{ matrix.xcode }} + runs-on: macos-${{ matrix.macOS }} + strategy: + matrix: + include: + - macOS: '14' + xcode: '15.4' + - macOS: '15' + xcode: '16.4' + env: + DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + key: ${{ runner.os }}-xcode-spm-${{ matrix.xcode }}-${{ hashFiles('Package.resolved') }} + restore-keys: ${{ runner.os }}-xcode-spm-${{ matrix.xcode }}- + path: .build + - name: Run tests + run: make spm_test diff --git a/.gitignore b/.gitignore index 1ed5560422..e6d5048bee 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ Packages/ # Bundler .bundle/ bundle/ +bin/ # Bazel /bazel-* diff --git a/Makefile b/Makefile index ecb313108c..d96690f97b 100644 --- a/Makefile +++ b/Makefile @@ -188,6 +188,12 @@ docs: bundle_install swift run swiftlint generate-docs bundle exec jazzy +docs_linux: bundle_install + bundle binstubs jazzy + ./bazel-bin/swiftlint generate-docs + ./bazel-bin/external/sourcekitten~/sourcekitten doc --spm --module-name SwiftLintCore > doc.json + ./bin/jazzy --sourcekitten-sourcefile doc.json + get_version: @echo "$(VERSION_STRING)" diff --git a/SwiftLint.podspec b/SwiftLint.podspec index 2241e1a140..03516af682 100644 --- a/SwiftLint.podspec +++ b/SwiftLint.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'SwiftLint' - s.version = `make get_version` + s.version = '0.59.1' s.summary = 'A tool to enforce Swift style and conventions.' s.homepage = 'https://github.com/realm/SwiftLint' s.license = { type: 'MIT', file: 'LICENSE' } diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 75578e801f..0000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,55 +0,0 @@ -trigger: -- main - -variables: - SKIP_INTEGRATION_TESTS: 'true' - -jobs: -- job: tests_macos - displayName: 'Tests, macOS' - strategy: - maxParallel: '10' - matrix: - '14, Xcode 15.4': - image: 'macOS-14' - xcode: '15.4' - # '14, Xcode 16.3': Runs on Buildkite. - '15, Xcode 16.4': - image: 'macOS-15' - xcode: '16.4' - pool: - vmImage: $(image) - variables: - DEVELOPER_DIR: /Applications/Xcode_$(xcode).app - steps: - - script: make spm_test - displayName: Run tests - -- job: Jazzy - pool: - vmImage: 'macOS-14' - variables: - DEVELOPER_DIR: /Applications/Xcode_15.4.app - steps: - - script: make docs - displayName: Run Jazzy - - script: > - if ruby -rjson -e "j = JSON.parse(File.read('docs/undocumented.json')); exit j['warnings'].length != 0"; then - echo "Undocumented declarations:" - cat docs/undocumented.json - exit 1 - fi - displayName: Validate documentation coverage - - task: PublishPipelineArtifact@0 - inputs: - artifactName: 'API Docs' - targetPath: 'docs' - displayName: Publish API docs - - task: DownloadSecureFile@1 - condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') - inputs: - secureFile: doc_deploy_key - displayName: Download deploy key - - script: ./tools/push-docs - displayName: Push documentation to GitHub Pages - condition: eq(variables['Build.SourceBranch'], 'refs/heads/main') diff --git a/tools/push-docs b/tools/push-docs deleted file mode 100755 index 4c9e090e7b..0000000000 --- a/tools/push-docs +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -mkdir -p ~/.ssh && mv $DOWNLOADSECUREFILE_SECUREFILEPATH ~/.ssh/id_rsa -chmod 700 ~/.ssh && chmod 600 ~/.ssh/id_rsa -ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts - -source_sha="$(git rev-parse HEAD)" -user="swiftlintbot@jpsim.com" -git config --global user.email "$user" -git config --global user.name "$user" -git clone git@github.com:realm/SwiftLint.git out - -cd out -git checkout gh-pages -git rm -rf . -rm -rf Carthage -cd .. - -cp -a docs/. out/. -cd out - -git add -A -git commit -m "Automated deployment to GitHub Pages: ${source_sha}" --allow-empty - -git push origin gh-pages From 4efdcc7b25bb6e098fa836b0275b7a538533b551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 11 Jul 2025 00:13:06 +0200 Subject: [PATCH 62/80] Base visitors on rule configuration (#6159) --- .../Rules/Metrics/ClosureBodyLengthRule.swift | 2 +- .../Metrics/FunctionBodyLengthRule.swift | 2 +- .../Rules/Metrics/TypeBodyLengthRule.swift | 2 +- .../Visitors/BodyLengthVisitor.swift | 20 ++++++++++++++----- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift index 2ecb9ed825..97d316d870 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/ClosureBodyLengthRule.swift @@ -22,7 +22,7 @@ struct ClosureBodyLengthRule: Rule { } private extension ClosureBodyLengthRule { - final class Visitor: BodyLengthVisitor { + final class Visitor: BodyLengthVisitor { override func visitPost(_ node: ClosureExprSyntax) { registerViolations( leftBrace: node.leftBrace, diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift index 60930c44f8..1393a61563 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/FunctionBodyLengthRule.swift @@ -104,7 +104,7 @@ struct FunctionBodyLengthRule: Rule { } private extension FunctionBodyLengthRule { - final class Visitor: BodyLengthVisitor { + final class Visitor: BodyLengthVisitor { override func visitPost(_ node: DeinitializerDeclSyntax) { if let body = node.body { registerViolations( diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index 6565ca0752..78bb89e42c 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -36,7 +36,7 @@ struct TypeBodyLengthRule: Rule { } private extension TypeBodyLengthRule { - final class Visitor: BodyLengthVisitor { + final class Visitor: BodyLengthVisitor { override func visitPost(_ node: ActorDeclSyntax) { collectViolation(node) } diff --git a/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift b/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift index d303599692..2f75e4b173 100644 --- a/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift +++ b/Source/SwiftLintCore/Visitors/BodyLengthVisitor.swift @@ -1,9 +1,19 @@ import SwiftSyntax +/// A configuration that's based on warning and error thresholds for violations. +public protocol SeverityLevelsBasedRuleConfiguration: RuleConfiguration { + /// The severity configuration that defines the thresholds for warning and error severities. + var severityConfiguration: SeverityLevelsConfiguration { get } +} + +extension SeverityLevelsConfiguration: SeverityLevelsBasedRuleConfiguration { + public var severityConfiguration: SeverityLevelsConfiguration { self } +} + /// Violation visitor customized to collect violations of code blocks that exceed a specified number of lines. -open class BodyLengthVisitor: ViolationsSyntaxVisitor> { +open class BodyLengthVisitor: ViolationsSyntaxVisitor { @inlinable - override public init(configuration: SeverityLevelsConfiguration, file: SwiftLintFile) { + override public init(configuration: LevelConfig, file: SwiftLintFile) { super.init(configuration: configuration, file: file) } @@ -25,12 +35,12 @@ open class BodyLengthVisitor: ViolationsSyntaxVisitor error { + if let error = configuration.severityConfiguration.error, lineCount > error { severity = .error upperBound = error - } else if lineCount > configuration.warning { + } else if lineCount > configuration.severityConfiguration.warning { severity = .warning - upperBound = configuration.warning + upperBound = configuration.severityConfiguration.warning } else { return } From 092d0c3b628d330277e662a0ceedd5ed70567602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 11 Jul 2025 10:18:28 +0200 Subject: [PATCH 63/80] Keep severity levels unchanged when no options are configured (#6160) --- CHANGELOG.md | 5 + .../SeverityLevelsConfiguration.swift | 5 +- ...clomaticComplexityConfigurationTests.swift | 3 +- .../LineLengthConfigurationTests.swift | 55 +++--- .../SeverityLevelsConfigurationTests.swift | 166 ++++++++++++++++++ 5 files changed, 198 insertions(+), 36 deletions(-) create mode 100644 Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ee743219..87246f0589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,11 @@ or other error. [Martin Redington](https://github.com/mildm8nnered) [#6052](https://github.com/realm/SwiftLint/issues/6052) + +* Keep the default severity levels when neither `warning` nor `error` values are configured. + Ensure especially that the `error` level is not set to `nil` when the `warning` level + isn't set either. + [SimplyDanny](https://github.com/SimplyDanny) ## 0.59.1: Crisp Spring Clean diff --git a/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift index e23f082b05..64b7797541 100644 --- a/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/SeverityLevelsConfiguration.swift @@ -32,7 +32,8 @@ public struct SeverityLevelsConfiguration: RuleConfiguration, Inli warning = configurationArray[0] error = (configurationArray.count > 1) ? configurationArray[1] : nil } else if let configDict = configuration as? [String: Any?] { - if let warningValue = configDict[$warning.key] { + let warningValue = configDict[$warning.key] + if let warningValue { if let warning = warningValue as? Int { self.warning = warning } else { @@ -47,7 +48,7 @@ public struct SeverityLevelsConfiguration: RuleConfiguration, Inli } else { throw Issue.invalidConfiguration(ruleID: Parent.identifier) } - } else { + } else if warningValue != nil { self.error = nil } } else { diff --git a/Tests/BuiltInRulesTests/CyclomaticComplexityConfigurationTests.swift b/Tests/BuiltInRulesTests/CyclomaticComplexityConfigurationTests.swift index d81f08b69b..0bc2d69291 100644 --- a/Tests/BuiltInRulesTests/CyclomaticComplexityConfigurationTests.swift +++ b/Tests/BuiltInRulesTests/CyclomaticComplexityConfigurationTests.swift @@ -46,7 +46,6 @@ final class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { let error2 = 40 let length2 = SeverityLevelsConfiguration(warning: warning2, error: error2) let config2: [String: Int] = ["warning": warning2, "error": error2] - let length3 = SeverityLevelsConfiguration(warning: warning2) let config3: [String: Bool] = ["ignores_case_statements": false] try configuration.apply(configuration: config1) @@ -58,7 +57,7 @@ final class CyclomaticComplexityConfigurationTests: SwiftLintTestCase { XCTAssertTrue(configuration.ignoresCaseStatements) try configuration.apply(configuration: config3) - XCTAssertEqual(configuration.length, length3) + XCTAssertEqual(configuration.length, length2) XCTAssertFalse(configuration.ignoresCaseStatements) } diff --git a/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift b/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift index abdbe9628b..2f4dd25ec3 100644 --- a/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift +++ b/Tests/BuiltInRulesTests/LineLengthConfigurationTests.swift @@ -99,7 +99,7 @@ final class LineLengthConfigurationTests: SwiftLintTestCase { } } - func testLineLengthConfigurationApplyConfigurationWithArray() { + func testLineLengthConfigurationApplyConfigurationWithArray() throws { var configuration = LineLengthConfiguration(length: SeverityLevelsConfiguration(warning: 0, error: 0)) let warning1 = 100 @@ -111,18 +111,14 @@ final class LineLengthConfigurationTests: SwiftLintTestCase { let length2 = SeverityLevelsConfiguration(warning: warning2, error: nil) let config2 = [warning2] - do { - try configuration.apply(configuration: config1) - XCTAssertEqual(configuration.length, length1) + try configuration.apply(configuration: config1) + XCTAssertEqual(configuration.length, length1) - try configuration.apply(configuration: config2) - XCTAssertEqual(configuration.length, length2) - } catch { - XCTFail("Failed to apply configuration with array") - } + try configuration.apply(configuration: config2) + XCTAssertEqual(configuration.length, length2) } - func testLineLengthConfigurationApplyConfigurationWithDictionary() { + func testLineLengthConfigurationApplyConfigurationWithDictionary() throws { var configuration = LineLengthConfiguration(length: SeverityLevelsConfiguration(warning: 0, error: 0)) let warning1 = 100 @@ -141,34 +137,29 @@ final class LineLengthConfigurationTests: SwiftLintTestCase { let length2 = SeverityLevelsConfiguration(warning: warning2, error: error2) let config2: [String: Int] = ["warning": warning2, "error": error2] - let length3 = SeverityLevelsConfiguration(warning: warning2) let config3: [String: Bool] = [ "ignores_urls": false, "ignores_function_declarations": false, "ignores_comments": false, ] - do { - try configuration.apply(configuration: config1) - XCTAssertEqual(configuration.length, length1) - XCTAssertTrue(configuration.ignoresURLs) - XCTAssertTrue(configuration.ignoresFunctionDeclarations) - XCTAssertTrue(configuration.ignoresComments) - - try configuration.apply(configuration: config2) - XCTAssertEqual(configuration.length, length2) - XCTAssertTrue(configuration.ignoresURLs) - XCTAssertTrue(configuration.ignoresFunctionDeclarations) - XCTAssertTrue(configuration.ignoresComments) - - try configuration.apply(configuration: config3) - XCTAssertEqual(configuration.length, length3) - XCTAssertFalse(configuration.ignoresURLs) - XCTAssertFalse(configuration.ignoresFunctionDeclarations) - XCTAssertFalse(configuration.ignoresComments) - } catch { - XCTFail("Failed to apply configuration with dictionary") - } + try configuration.apply(configuration: config1) + XCTAssertEqual(configuration.length, length1) + XCTAssertTrue(configuration.ignoresURLs) + XCTAssertTrue(configuration.ignoresFunctionDeclarations) + XCTAssertTrue(configuration.ignoresComments) + + try configuration.apply(configuration: config2) + XCTAssertEqual(configuration.length, length2) + XCTAssertTrue(configuration.ignoresURLs) + XCTAssertTrue(configuration.ignoresFunctionDeclarations) + XCTAssertTrue(configuration.ignoresComments) + + try configuration.apply(configuration: config3) + XCTAssertEqual(configuration.length, length2) + XCTAssertFalse(configuration.ignoresURLs) + XCTAssertFalse(configuration.ignoresFunctionDeclarations) + XCTAssertFalse(configuration.ignoresComments) } func testLineLengthConfigurationCompares() { diff --git a/Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift b/Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift new file mode 100644 index 0000000000..43c248e1dc --- /dev/null +++ b/Tests/FrameworkTests/SeverityLevelsConfigurationTests.swift @@ -0,0 +1,166 @@ +@testable import SwiftLintCore +import TestHelpers +import XCTest + +struct MockSeverityLevelsRule: Rule { + static let identifier = "test_severity_levels" + static let description = RuleDescription( + identifier: identifier, + name: "Test Severity Levels", + description: "A test rule for SeverityLevelsConfiguration", + kind: .style + ) + + var configuration = SeverityLevelsConfiguration(warning: 12, error: nil) + + func validate(file _: SwiftLintFile) -> [StyleViolation] { + [] + } +} + +final class SeverityLevelsConfigurationTests: SwiftLintTestCase { + func testInitializationWithWarningOnly() { + let config = SeverityLevelsConfiguration(warning: 10) + XCTAssertEqual(config.warning, 10) + XCTAssertNil(config.error) + + let params = config.params + XCTAssertEqual(params.count, 1) + XCTAssertEqual(params[0].severity, .warning) + XCTAssertEqual(params[0].value, 10) + } + + func testInitializationWithWarningAndError() { + let config = SeverityLevelsConfiguration(warning: 10, error: 20) + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 20) + + let params = config.params + XCTAssertEqual(params.count, 2) + XCTAssertEqual(params[0].severity, .error) + XCTAssertEqual(params[0].value, 20) + XCTAssertEqual(params[1].severity, .warning) + XCTAssertEqual(params[1].value, 10) + } + + func testApplyConfigurationWithSingleElementArray() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: [15]) + + XCTAssertEqual(config.warning, 15) + XCTAssertNil(config.error) + } + + func testApplyConfigurationWithTwoElementArray() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: [10, 25]) + + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 25) + } + + func testApplyConfigurationWithMultipleElementArray() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: [10, 25, 50]) + + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 25) // Only first two elements are used + } + + func testApplyConfigurationWithEmptyArray() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.nothingApplied(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: [] as [Int]) + } + } + + func testApplyConfigurationWithInvalidArrayType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.nothingApplied(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: ["invalid"]) + } + } + + func testApplyConfigurationWithWarningOnlyDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: ["warning": 15]) + + XCTAssertEqual(config.warning, 15) + XCTAssertNil(config.error) + } + + func testApplyConfigurationWithWarningAndErrorDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 0, error: 0) + + try config.apply(configuration: ["warning": 10, "error": 25]) + + XCTAssertEqual(config.warning, 10) + XCTAssertEqual(config.error, 25) + } + + func testApplyConfigurationWithErrorOnlyDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + try config.apply(configuration: ["error": 25]) + + XCTAssertEqual(config.warning, 12) // Should remain unchanged + XCTAssertEqual(config.error, 25) + } + + func testApplyConfigurationWithNilErrorDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 10, error: 20) + + try config.apply(configuration: ["error": nil as Int?]) + + XCTAssertEqual(config.warning, 10) + XCTAssertNil(config.error) + } + + func testApplyConfigurationWithWarningSetToNilError() throws { + var config = SeverityLevelsConfiguration(warning: 10, error: 20) + + try config.apply(configuration: ["warning": 15]) + + XCTAssertEqual(config.warning, 15) + XCTAssertNil(config.error) // Should be set to nil when warning is specified without error + } + + func testApplyConfigurationWithInvalidWarningType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.invalidConfiguration(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: ["warning": "invalid"]) + } + } + + func testApplyConfigurationWithInvalidErrorType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.invalidConfiguration(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: ["error": "invalid"]) + } + } + + func testApplyConfigurationWithInvalidConfigurationType() { + var config = SeverityLevelsConfiguration(warning: 12, error: nil) + + checkError(Issue.nothingApplied(ruleID: MockSeverityLevelsRule.identifier)) { + try config.apply(configuration: "invalid") + } + } + + func testApplyConfigurationWithEmptyDictionary() throws { + var config = SeverityLevelsConfiguration(warning: 12, error: 15) + + try config.apply(configuration: [:] as [String: Any]) + + XCTAssertEqual(config.warning, 12) + XCTAssertEqual(config.error, 15) // Should remain unchanged when nothing is applied + } +} From a321566c05b282de29cf44d8aa5f6e8c779d57d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 11 Jul 2025 22:27:47 +0200 Subject: [PATCH 64/80] Support protocols and extensions in type body length checking (#6143) --- .swiftlint.yml | 4 +- CHANGELOG.md | 7 + .../Rules/Metrics/TypeBodyLengthRule.swift | 142 ++++++++++++++---- .../TypeBodyLengthConfiguration.swift | 25 +++ .../TypeBodyLengthConfigurationTests.swift | 91 +++++++++++ .../TypeBodyLengthRuleTests.swift | 73 +++++++++ .../ConfigurationTests+MultipleConfigs.swift | 1 + .../default_rule_configurations.yml | 1 + 8 files changed, 314 insertions(+), 30 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift create mode 100644 Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift create mode 100644 Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index a4d6836284..5c349312a8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -89,7 +89,9 @@ redundant_type_annotation: single_test_class: *unit_test_configuration trailing_comma: mandatory_comma: true -type_body_length: 400 +type_body_length: + warning: 400 + excluded_types: [] unneeded_override: affect_initializers: true unused_import: diff --git a/CHANGELOG.md b/CHANGELOG.md index 87246f0589..4f192aee1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ [imsonalbajaj](https://github.com/imsonalbajaj) [#6054](https://github.com/realm/SwiftLint/issues/6054) +* Support extensions and protocols in `type_body_length` rule. They can be configured using the + new `excluded_types` option which by default excludes `extension` and `protocol` types. + This means the rule now checks `struct`, `class`, `actor` and `enum` by default. To enable + checking of extensions and protocols, set `excluded_types` to an empty array or exclude other + types as needed. + [SimplyDanny](https://github.com/SimplyDanny) + * Ignore various assignment operators like `=`, `+=`, `&=`, etc. with right-hand side ternary expressions otherwise violating the `void_function_in_ternary` rule. [SimplyDanny](https://github.com/SimplyDanny) diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift index 78bb89e42c..47aa4f0211 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/TypeBodyLengthRule.swift @@ -1,56 +1,140 @@ import SwiftSyntax -private func wrapExample( - prefix: String = "", - _ type: String, - _ template: String, - _ count: Int, - _ add: String = "", - file: StaticString = #filePath, - line: UInt = #line) -> Example { - Example("\(prefix)\(type) Abc {\n" + - repeatElement(template, count: count).joined() + "\(add)}\n", file: file, line: line) -} - @SwiftSyntaxRule struct TypeBodyLengthRule: Rule { - var configuration = SeverityLevelsConfiguration(warning: 250, error: 350) + var configuration = TypeBodyLengthConfiguration() + + private static let testConfig = ["warning": 2] as [String: any Sendable] + private static let testConfigWithAllTypes = testConfig.merging( + ["excluded_types": [] as [String]], + uniquingKeysWith: { $1 } + ) static let description = RuleDescription( identifier: "type_body_length", name: "Type Body Length", description: "Type bodies should not span too many lines", kind: .metrics, - nonTriggeringExamples: ["class", "struct", "enum", "actor"].flatMap({ type in - [ - wrapExample(type, "let abc = 0\n", 249), - wrapExample(type, "\n", 251), - wrapExample(type, "// this is a comment\n", 251), - wrapExample(type, "let abc = 0\n", 249, "\n/* this is\na multiline comment\n*/"), - ] - }), - triggeringExamples: ["class", "struct", "enum", "actor"].map({ type in - wrapExample(prefix: "↓", type, "let abc = 0\n", 251) - }) + nonTriggeringExamples: [ + Example("actor A {}", configuration: testConfig), + Example("class C {}", configuration: testConfig), + Example("enum E {}", configuration: testConfig), + Example("extension E {}", configuration: testConfigWithAllTypes), + Example("protocol P {}", configuration: testConfigWithAllTypes), + Example("struct S {}", configuration: testConfig), + Example(""" + actor A { + let x = 0 + } + """, configuration: testConfig), + Example(""" + class C { + let x = 0 + // comments + // will + // be + // ignored + } + """, configuration: testConfig), + Example(""" + enum E { + let x = 0 + // empty lines will be ignored + + + } + """, configuration: testConfig), + Example(""" + protocol P { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + ], + triggeringExamples: [ + Example(""" + ↓actor A { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + Example(""" + ↓class C { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + Example(""" + ↓enum E { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + Example(""" + ↓extension E { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfigWithAllTypes), + Example(""" + ↓protocol P { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfigWithAllTypes), + Example(""" + ↓struct S { + let x = 0 + let y = 1 + let z = 2 + } + """, configuration: testConfig), + ] ) } private extension TypeBodyLengthRule { final class Visitor: BodyLengthVisitor { override func visitPost(_ node: ActorDeclSyntax) { - collectViolation(node) + if !configuration.excludedTypes.contains(.actor) { + collectViolation(node) + } + } + + override func visitPost(_ node: ClassDeclSyntax) { + if !configuration.excludedTypes.contains(.class) { + collectViolation(node) + } } override func visitPost(_ node: EnumDeclSyntax) { - collectViolation(node) + if !configuration.excludedTypes.contains(.enum) { + collectViolation(node) + } } - override func visitPost(_ node: ClassDeclSyntax) { - collectViolation(node) + override func visitPost(_ node: ExtensionDeclSyntax) { + if !configuration.excludedTypes.contains(.extension) { + collectViolation(node) + } + } + + override func visitPost(_ node: ProtocolDeclSyntax) { + if !configuration.excludedTypes.contains(.protocol) { + collectViolation(node) + } } override func visitPost(_ node: StructDeclSyntax) { - collectViolation(node) + if !configuration.excludedTypes.contains(.struct) { + collectViolation(node) + } } private func collectViolation(_ node: some DeclGroupSyntax) { diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift new file mode 100644 index 0000000000..c8454724f1 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/TypeBodyLengthConfiguration.swift @@ -0,0 +1,25 @@ +import SwiftLintCore + +@AcceptableByConfigurationElement +enum TypeBodyLengthCheckType: String, CaseIterable, Comparable { + case `actor` = "actor" + case `class` = "class" + case `enum` = "enum" + case `extension` = "extension" + case `protocol` = "protocol" + case `struct` = "struct" + + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +@AutoConfigParser +struct TypeBodyLengthConfiguration: SeverityLevelsBasedRuleConfiguration { + typealias Parent = TypeBodyLengthRule + + @ConfigurationElement(inline: true) + private(set) var severityConfiguration = SeverityLevelsConfiguration(warning: 250, error: 350) + @ConfigurationElement(key: "excluded_types") + private(set) var excludedTypes = Set([.extension, .protocol]) +} diff --git a/Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift b/Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift new file mode 100644 index 0000000000..bf1b72ce93 --- /dev/null +++ b/Tests/BuiltInRulesTests/TypeBodyLengthConfigurationTests.swift @@ -0,0 +1,91 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class TypeBodyLengthConfigurationTests: SwiftLintTestCase { + func testDefaultConfiguration() { + let config = TypeBodyLengthConfiguration() + XCTAssertEqual(config.severityConfiguration.warning, 250) + XCTAssertEqual(config.severityConfiguration.error, 350) + XCTAssertEqual(config.excludedTypes, [.extension, .protocol]) + } + + func testApplyingCustomConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "warning": 150, + "error": 200, + "excluded_types": ["struct", "class"], + ] as [String: any Sendable] + ) + XCTAssertEqual(config.severityConfiguration.warning, 150) + XCTAssertEqual(config.severityConfiguration.error, 200) + XCTAssertEqual(config.excludedTypes, Set([.struct, .class])) + } + + func testApplyingOnlyExcludedTypesConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": ["actor", "enum"] + ] as [String: any Sendable] + ) + + // Severity should remain default + XCTAssertEqual(config.severityConfiguration.warning, 250) + XCTAssertEqual(config.severityConfiguration.error, 350) + + // Excluded types should be updated + XCTAssertEqual(config.excludedTypes, Set([.actor, .enum])) + } + + func testApplyingAllTypesAsExcludedConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": ["struct", "class", "actor", "enum", "extension", "protocol"] + ] as [String: any Sendable] + ) + XCTAssertEqual(config.excludedTypes, Set(TypeBodyLengthCheckType.allCases)) + } + + func testApplyingEmptyExcludedTypesConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": [] as [String] + ] as [String: any Sendable] + ) + XCTAssertTrue(config.excludedTypes.isEmpty) + } + + func testApplyingSingleExcludedTypeConfiguration() throws { + var config = TypeBodyLengthConfiguration() + try config.apply( + configuration: [ + "excluded_types": ["extension"] + ] as [String: any Sendable] + ) + XCTAssertEqual(config.excludedTypes, Set([.extension])) + } + + func testInvalidExcludedTypeConfiguration() throws { + var config = TypeBodyLengthConfiguration() + checkError(Issue.invalidConfiguration(ruleID: TypeBodyLengthRule.identifier)) { + try config.apply( + configuration: [ + "excluded_types": ["invalid_type"] + ] as [String: any Sendable] + ) + } + XCTAssertEqual(config.excludedTypes, Set([.extension, .protocol])) + } + + func testTypeEnumComparability() { + XCTAssertEqual( + TypeBodyLengthCheckType.allCases.sorted(), + [.actor, .class, .enum, .extension, .protocol, .struct] + ) + } +} diff --git a/Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift b/Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift new file mode 100644 index 0000000000..9b2e04c769 --- /dev/null +++ b/Tests/BuiltInRulesTests/TypeBodyLengthRuleTests.swift @@ -0,0 +1,73 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class TypeBodyLengthRuleTests: SwiftLintTestCase { + func testWarning() { + let example = Example(""" + actor A { + let x = 0 + let y = 1 + let z = 2 + } + """) + + XCTAssertEqual( + self.violations(example, configuration: ["warning": 2, "error": 4]), + [ + StyleViolation( + ruleDescription: TypeBodyLengthRule.description, + severity: .warning, + location: Location(file: nil, line: 1, character: 1), + reason: """ + Actor body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ + ), + ] + ) + } + + func testError() { + let example = Example(""" + class C { + let x = 0 + let y = 1 + let z = 2 + } + """) + + XCTAssertEqual( + self.violations(example, configuration: ["warning": 1, "error": 2]), + [ + StyleViolation( + ruleDescription: TypeBodyLengthRule.description, + severity: .error, + location: Location(file: nil, line: 1, character: 1), + reason: """ + Class body should span 2 lines or less excluding comments and \ + whitespace: currently spans 3 lines + """ + ), + ] + ) + } + + func testViolationMessages() { + let types = TypeBodyLengthRule.description.triggeringExamples.flatMap { + self.violations($0, configuration: ["warning": 2]) + }.compactMap { + $0.reason.split(separator: " ", maxSplits: 1).first + } + + XCTAssertEqual( + types, + ["Actor", "Class", "Enum", "Extension", "Protocol", "Struct"] + ) + } + + private func violations(_ example: Example, configuration: Any? = nil) -> [StyleViolation] { + let config = makeConfig(configuration, TypeBodyLengthRule.identifier)! + return TestHelpers.violations(example, config: config) + } +} diff --git a/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift b/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift index 43dadadc62..08efda167f 100644 --- a/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift +++ b/Tests/FrameworkTests/ConfigurationTests+MultipleConfigs.swift @@ -10,6 +10,7 @@ private extension Configuration { } } +// swiftlint:disable:next type_body_length extension ConfigurationTests { // MARK: - Rules Merging func testMerge() { diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 7bc328d8ca..e30bdc4b4b 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -1198,6 +1198,7 @@ trailing_whitespace: type_body_length: warning: 250 error: 350 + excluded_types: [extension, protocol] meta: opt-in: false correctable: false From 8229f45de49f9256968525a54f72230b8b73c21f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 11 Jul 2025 23:40:08 +0200 Subject: [PATCH 65/80] Update dependencies (#6162) --- MODULE.bazel | 4 ++-- Package.resolved | 12 ++++++------ Package.swift | 6 +++--- bazel/repos.bzl | 18 +++++++++--------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 31c22c5ef1..c92550eb2f 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -13,9 +13,9 @@ bazel_dep(name = "rules_cc", version = "0.1.1") bazel_dep(name = "rules_shell", version = "0.4.0", repo_name = "build_bazel_rules_shell") bazel_dep(name = "rules_swift", version = "2.8.1", max_compatibility_level = 3, repo_name = "build_bazel_rules_swift") bazel_dep(name = "sourcekitten", version = "0.37.2", repo_name = "com_github_jpsim_sourcekitten") -bazel_dep(name = "swift_argument_parser", version = "1.5.0", repo_name = "com_github_apple_swift_argument_parser") +bazel_dep(name = "swift_argument_parser", version = "1.6.1", repo_name = "com_github_apple_swift_argument_parser") bazel_dep(name = "swift-syntax", version = "601.0.1.1", repo_name = "SwiftSyntax") -bazel_dep(name = "yams", version = "6.0.1", repo_name = "com_github_jpsim_yams") +bazel_dep(name = "yams", version = "6.0.2", repo_name = "com_github_jpsim_yams") swiftlint_repos = use_extension("//bazel:repos.bzl", "swiftlint_repos_bzlmod") use_repo( diff --git a/Package.resolved b/Package.resolved index f191f6b728..ce663816cd 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "729e01bc9b9dab466ac85f21fb9ee2bc1c61b258", - "version" : "1.8.4" + "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f", + "version" : "1.9.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", - "version" : "1.5.1" + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { - "revision" : "7568d1c6c63a094405afb32264c57dc4e1435835", - "version" : "6.0.1" + "revision" : "f4d4d6827d36092d151ad7f6fef1991c1b7192f6", + "version" : "6.0.2" } } ], diff --git a/Package.swift b/Package.swift index 2f7dc042db..5a9b74765e 100644 --- a/Package.swift +++ b/Package.swift @@ -32,13 +32,13 @@ let package = Package( .plugin(name: "SwiftLintCommandPlugin", targets: ["SwiftLintCommandPlugin"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.5.1")), + .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.6.1")), .package(url: "https://github.com/swiftlang/swift-syntax.git", exact: "601.0.1"), .package(url: "https://github.com/jpsim/SourceKitten.git", .upToNextMajor(from: "0.37.2")), - .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "6.0.1")), + .package(url: "https://github.com/jpsim/Yams.git", .upToNextMajor(from: "6.0.2")), .package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", .upToNextMajor(from: "0.9.0")), .package(url: "https://github.com/JohnSundell/CollectionConcurrencyKit.git", .upToNextMajor(from: "0.2.0")), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.8.4")), + .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMajor(from: "1.9.0")), ], targets: [ .executableTarget( diff --git a/bazel/repos.bzl b/bazel/repos.bzl index 760d8aded9..bdc6080bcd 100644 --- a/bazel/repos.bzl +++ b/bazel/repos.bzl @@ -19,17 +19,17 @@ def swiftlint_repos(bzlmod = False): http_archive( name = "com_github_apple_swift_argument_parser", - url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.5.0.tar.gz", + url = "https://github.com/apple/swift-argument-parser/archive/refs/tags/1.6.1.tar.gz", build_file = "@SwiftLint//bazel:SwiftArgumentParser.BUILD", - sha256 = "946a4cf7bdd2e4f0f8b82864c56332238ba3f0a929c6d1a15f55affdb10634e6", - strip_prefix = "swift-argument-parser-1.5.0", + sha256 = "d2fbb15886115bb2d9bfb63d4c1ddd4080cbb4bfef2651335c5d3b9dd5f3c8ba", + strip_prefix = "swift-argument-parser-1.6.1", ) http_archive( name = "com_github_jpsim_yams", - url = "https://github.com/jpsim/Yams/releases/download/6.0.1/Yams-6.0.1.tar.gz", - sha256 = "76afe79db05acb0eda4910e0b9da6a8562ad6139ac317daa747cd829beb93b9e", - strip_prefix = "Yams-6.0.1", + url = "https://github.com/jpsim/Yams/archive/refs/tags/6.0.2.tar.gz", + sha256 = "a1ae9733755f77fd56e4b01081baea2a756d8cd4b6b7ec58dd971b249318df48", + strip_prefix = "Yams-6.0.2", ) http_archive( @@ -58,10 +58,10 @@ def swiftlint_repos(bzlmod = False): http_archive( name = "swiftlint_com_github_krzyzanowskim_cryptoswift", - sha256 = "69b23102ff453990d03aff4d3fabd172d0667b2b3ed95730021d60a0f8d50d14", + sha256 = "81b1ba186e2edcff47bcc2a3b6a242df083ba2f64bfb42209f79090cb8d7f889", build_file = "@SwiftLint//bazel:CryptoSwift.BUILD", - strip_prefix = "CryptoSwift-1.8.4", - url = "https://github.com/krzyzanowskim/CryptoSwift/archive/refs/tags/1.8.4.tar.gz", + strip_prefix = "CryptoSwift-1.9.0", + url = "https://github.com/krzyzanowskim/CryptoSwift/archive/refs/tags/1.9.0.tar.gz", ) def _swiftlint_repos_bzlmod(_): From cb214d51fa002f53e6a0f385b2ed0837e639f5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 12 Jul 2025 13:52:50 +0200 Subject: [PATCH 66/80] Add new `prefer_condition_list` rule (#6157) --- .swiftlint.yml | 1 + CHANGELOG.md | 5 + .../Models/BuiltInRules.swift | 1 + .../Idiomatic/PreferConditionListRule.swift | 211 ++++++++++++++++++ Tests/GeneratedTests/GeneratedTests_06.swift | 12 +- Tests/GeneratedTests/GeneratedTests_07.swift | 12 +- Tests/GeneratedTests/GeneratedTests_08.swift | 12 +- Tests/GeneratedTests/GeneratedTests_09.swift | 12 +- Tests/GeneratedTests/GeneratedTests_10.swift | 6 + .../default_rule_configurations.yml | 5 + 10 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 5c349312a8..97952b8544 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -37,6 +37,7 @@ disabled_rules: - no_grouping_extension - no_magic_numbers - one_declaration_per_file + - prefer_condition_list - prefer_key_path # Re-enable once we are on Swift 6. - prefer_nimble - prefixed_toplevel_constant diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f192aee1a..694500781c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,11 @@ * Improve `multiline_parameters` rule to correctly support `max_number_of_single_line_parameters` and detect mixed formatting. [GandaLF2006](https://github.com/GandaLF2006) + +* Add new `prefer_condition_list` rule that triggers when a `guard`/`if`/`while` + condition is composed of multiple expressions connected by the `&&` operator. + It suggests to use a condition list instead, which is more idiomatic. + [SimplyDanny](https://github.com/SimplyDanny) * Add `ignore_coding_keys` parameter to `nesting` rule. Setting this to true prevents `CodingKey` enums from violating the rule. diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 0138cc743e..bea8255b9c 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -148,6 +148,7 @@ public let builtInRules: [any Rule.Type] = [ OverrideInExtensionRule.self, PatternMatchingKeywordsRule.self, PeriodSpacingRule.self, + PreferConditionListRule.self, PreferKeyPathRule.self, PreferNimbleRule.self, PreferSelfInStaticReferencesRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift new file mode 100644 index 0000000000..959e76b34b --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/PreferConditionListRule.swift @@ -0,0 +1,211 @@ +import SwiftLintCore +import SwiftSyntax + +@SwiftSyntaxRule(foldExpressions: true, explicitRewriter: true, optIn: true) +struct PreferConditionListRule: Rule { + var configuration = SeverityConfiguration(.warning) + + static let description = RuleDescription( + identifier: "prefer_condition_list", + name: "Prefer Condition List", + description: "Prefer a condition list over chaining conditions with '&&'", + rationale: """ + Instead of chaining conditions with `&&`, use a condition list to separate conditions with commas, that is, + use + + ``` + if a, b {} + ``` + + instead of + + ``` + if a && b {} + ``` + + Using a condition list improves readability and makes it easier to add or remove conditions in the future. + It also allows for better formatting and alignment of conditions. All in all, it's the idiomatic way to + write conditions in Swift. + + Since function calls with trailing closures trigger a warning in the Swift compiler when used in + conditions, this rule makes sure to wrap such expressions in parentheses when transforming them to + condition list elements. The scope of the parentheses is limited to the function call itself. + """, + kind: .idiomatic, + nonTriggeringExamples: [ + Example("if a, b {}"), + Example("guard a || b && c {}"), + Example("if a && b || c {}"), + Example("let result = a && b"), + Example("repeat {} while a && b"), + Example("if (f {}) {}"), + Example("if f {} {}"), + ], + triggeringExamples: [ + Example("if a ↓&& b {}"), + Example("if a ↓&& b ↓&& c {}"), + Example("while a ↓&& b {}"), + Example("guard a ↓&& b {}"), + Example("guard (a || b) ↓&& c {}"), + Example("if a ↓&& (b && c) {}"), + Example("guard a ↓&& b ↓&& c else {}"), + Example("if (a ↓&& b) {}"), + Example("if (a ↓&& f {}) {}"), + ], + corrections: [ + Example("if a && b {}"): + Example("if a, b {}"), + Example(""" + if a && + b {} + """): Example(""" + if a, + b {} + """), + Example("guard a && b && c else {}"): + Example("guard a, b, c else {}"), + Example("while a && b {}"): + Example("while a, b {}"), + Example("if a && b || c {}"): + Example("if a && b || c {}"), + Example("if (a && b) {}"): + Example("if a, b {}"), + Example("if a && (b && c) {}"): + Example("if a, b, c {}"), + Example("if (a && b) && c {}"): + Example("if a, b, c {}"), + Example("if (a && b), c {}"): + Example("if a, b, c {}"), + Example("guard (a || b) ↓&& c {}"): + Example("guard a || b, c {}"), + Example("if a && (b || c) {}"): + Example("if a, b || c {}"), + Example("if (a ↓&& f {}) {}"): + Example("if a, (f {}) {}"), + Example("if a ↓&& (b || f {}) {}"): + Example("if a, b || (f {}) {}"), + Example("if a ↓&& !f {} {}"): + Example("if a, !(f {}) {}"), + ] + ) +} + +private extension PreferConditionListRule { + final class Visitor: ViolationsSyntaxVisitor { + override func visitPost(_ node: ConditionElementSyntax) { + if case let .expression(expr) = node.condition { + collectViolations(for: expr) + } + } + + private func collectViolations(for expr: ExprSyntax) { + if let opExpr = expr.unwrap.as(InfixOperatorExprSyntax.self), + let opToken = opExpr.operator.as(BinaryOperatorExprSyntax.self)?.operator, + opToken.text == "&&" { + violations.append(opToken.positionAfterSkippingLeadingTrivia) + collectViolations(for: opExpr.leftOperand) // Expressions are left-recursive. + } + } + } + + private final class Rewriter: ViolationsSyntaxRewriter { + override func visit(_ node: ConditionElementListSyntax) -> ConditionElementListSyntax { + var elements = Array(node) + var modifiedIndices = Set() + var index = 0 + + while index < elements.count { + let element = elements[index] + guard case let .expression(expr) = element.condition else { + index += 1 + continue + } + if let opExpr = expr.as(InfixOperatorExprSyntax.self), + let opToken = opExpr.operator.as(BinaryOperatorExprSyntax.self)?.operator, + opToken.text == "&&" { + numberOfCorrections += 1 + + elements[index] = ConditionElementSyntax( + condition: .expression(opExpr.leftOperand.with(\.trailingTrivia, [])), + trailingComma: .commaToken(), + trailingTrivia: opToken.trailingTrivia + ) + modifiedIndices.insert(index) + + elements.insert( + ConditionElementSyntax( + condition: .expression(opExpr.rightOperand.with(\.trailingTrivia, [])), + trailingComma: index == elements.count - 1 ? nil : .commaToken(), + trailingTrivia: .space + ), + at: index + 1 + ) + modifiedIndices.insert(index + 1) + // Don't increment the index to re-evaluate `elements[index]`. + } else if expr.is(TupleExprSyntax.self) { + // Unwrap parenthesized expression and repeat the loop for the inner expression (i.e. without + // incrementing the index). + let unwrappedExpr = expr.unwrap + elements[index] = element.with(\.condition, .expression(unwrappedExpr)) + if unwrappedExpr != expr { + modifiedIndices.insert(index) + } + } else { + index += 1 + } + } + for (index, element) in elements.enumerated() where modifiedIndices.contains(index) { + if case let .expression(expr) = element.condition { + // If the expression contains function calls with trailing closures, we need to wrap them in + // parentheses. That might not be exactly how the author created the expression, but it is + // necessary to ensure no compiler warning appears after the transformations. + elements[index] = element.with( + \.condition, + .expression(ParenthesizedTrailingClosureRewriter().visit(expr)) + .with(\.leadingTrivia, expr.leadingTrivia) + .with(\.trailingTrivia, expr.trailingTrivia) + ) + } + } + return super.visit(ConditionElementListSyntax(elements)) + } + } +} + +private extension ExprSyntax { + var unwrap: ExprSyntax { + `as`(TupleExprSyntax.self)?.elements.onlyElement?.expression + .with(\.leadingTrivia, leadingTrivia) + .with(\.trailingTrivia, trailingTrivia) + ?? self + } +} + +private final class ParenthesizedTrailingClosureRewriter: SyntaxRewriter { + override func visitAny(_ node: Syntax) -> Syntax? { + if let opToken = node.as(InfixOperatorExprSyntax.self)?.operator.as(BinaryOperatorExprSyntax.self)?.operator, + ["&&", "||"].contains(opToken.text) { + nil + } else if let opToken = node.as(PrefixOperatorExprSyntax.self)?.operator, + ["!"].contains(opToken.text) { + nil + } else if node.is(FunctionCallExprSyntax.self) { + nil + } else { + node + } + } + + override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax { + if node.trailingClosure != nil || node.additionalTrailingClosures.isNotEmpty { + return ExprSyntax(TupleExprSyntax( + elements: LabeledExprListSyntax([ + LabeledExprSyntax(label: nil, expression: node.with(\.trailingTrivia, [])) + ]) + )) + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + } + return super.visit(node) + } +} diff --git a/Tests/GeneratedTests/GeneratedTests_06.swift b/Tests/GeneratedTests/GeneratedTests_06.swift index 2155d6d695..014ababfd4 100644 --- a/Tests/GeneratedTests/GeneratedTests_06.swift +++ b/Tests/GeneratedTests/GeneratedTests_06.swift @@ -133,6 +133,12 @@ final class PeriodSpacingRuleGeneratedTests: SwiftLintTestCase { } } +final class PreferConditionListRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferConditionListRule.description) + } +} + final class PreferKeyPathRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferKeyPathRule.description) @@ -150,9 +156,3 @@ final class PreferSelfInStaticReferencesRuleGeneratedTests: SwiftLintTestCase { verifyRule(PreferSelfInStaticReferencesRule.description) } } - -final class PreferSelfTypeOverTypeOfSelfRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(PreferSelfTypeOverTypeOfSelfRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_07.swift b/Tests/GeneratedTests/GeneratedTests_07.swift index 22605a9a9f..e503444dae 100644 --- a/Tests/GeneratedTests/GeneratedTests_07.swift +++ b/Tests/GeneratedTests/GeneratedTests_07.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class PreferSelfTypeOverTypeOfSelfRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(PreferSelfTypeOverTypeOfSelfRule.description) + } +} + final class PreferTypeCheckingRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(PreferTypeCheckingRule.description) @@ -150,9 +156,3 @@ final class RedundantSendableRuleGeneratedTests: SwiftLintTestCase { verifyRule(RedundantSendableRule.description) } } - -final class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(RedundantSetAccessControlRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_08.swift b/Tests/GeneratedTests/GeneratedTests_08.swift index e25c8f0d8f..068ffab49e 100644 --- a/Tests/GeneratedTests/GeneratedTests_08.swift +++ b/Tests/GeneratedTests/GeneratedTests_08.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(RedundantSetAccessControlRule.description) + } +} + final class RedundantStringEnumValueRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(RedundantStringEnumValueRule.description) @@ -150,9 +156,3 @@ final class SwitchCaseOnNewlineRuleGeneratedTests: SwiftLintTestCase { verifyRule(SwitchCaseOnNewlineRule.description) } } - -final class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(SyntacticSugarRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_09.swift b/Tests/GeneratedTests/GeneratedTests_09.swift index be7d5dd42d..1d8dc7614d 100644 --- a/Tests/GeneratedTests/GeneratedTests_09.swift +++ b/Tests/GeneratedTests/GeneratedTests_09.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class SyntacticSugarRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(SyntacticSugarRule.description) + } +} + final class TestCaseAccessibilityRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(TestCaseAccessibilityRule.description) @@ -150,9 +156,3 @@ final class UnusedDeclarationRuleGeneratedTests: SwiftLintTestCase { verifyRule(UnusedDeclarationRule.description) } } - -final class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedEnumeratedRule.description) - } -} diff --git a/Tests/GeneratedTests/GeneratedTests_10.swift b/Tests/GeneratedTests/GeneratedTests_10.swift index 3d0e2b5cbd..a5ce334fc5 100644 --- a/Tests/GeneratedTests/GeneratedTests_10.swift +++ b/Tests/GeneratedTests/GeneratedTests_10.swift @@ -7,6 +7,12 @@ @testable import SwiftLintCore import TestHelpers +final class UnusedEnumeratedRuleGeneratedTests: SwiftLintTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnusedEnumeratedRule.description) + } +} + final class UnusedImportRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedImportRule.description) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index e30bdc4b4b..aaeffb97b6 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -849,6 +849,11 @@ period_spacing: meta: opt-in: true correctable: true +prefer_condition_list: + severity: warning + meta: + opt-in: true + correctable: true prefer_key_path: severity: warning restrict_to_standard_functions: true From c1ffdfe8913c12398a3a05fe19844c1031fa7add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Sat, 12 Jul 2025 15:41:00 +0200 Subject: [PATCH 67/80] Enable `prefer_condition_list` rule (#6163) --- .swiftlint.yml | 1 - .../DiscouragedObjectLiteralRule.swift | 4 ++-- .../Idiomatic/RedundantVoidReturnRule.swift | 2 +- .../Lint/AccessibilityLabelForImageRule.swift | 2 +- .../Rules/Lint/EmptyXCTestMethodRule.swift | 2 +- .../Rules/Lint/OrphanedDocCommentRule.swift | 2 +- .../Rules/Lint/PrivateOutletRule.swift | 2 +- .../Rules/Lint/UnneededOverrideRule.swift | 2 +- .../Rules/Lint/UnusedDeclarationRule.swift | 2 +- .../Rules/Lint/UnusedImportRule.swift | 2 +- .../Rules/Metrics/FileLengthRule.swift | 4 ++-- .../Rules/Metrics/LineLengthRule.swift | 8 ++++---- .../Rules/Metrics/NestingRule.swift | 4 ++-- .../Rules/Performance/EmptyCountRule.swift | 2 +- .../OverriddenSuperCallConfiguration.swift | 2 +- .../ProhibitedSuperConfiguration.swift | 2 +- .../Rules/Style/AttributesRule.swift | 4 ++-- .../Rules/Style/ColonRule.swift | 6 +++--- .../Rules/Style/CommaRule.swift | 2 +- .../Rules/Style/FileHeaderRule.swift | 2 +- .../Rules/Style/IndentationWidthRule.swift | 2 +- .../MultilineParametersBracketsRule.swift | 2 +- .../Rules/Style/NumberSeparatorRule.swift | 2 +- .../Style/OperatorUsageWhitespaceRule.swift | 2 +- .../Style/ReturnArrowWhitespaceRule.swift | 4 ++-- .../Rules/Style/StatementPositionRule.swift | 2 +- .../Rules/Style/SwitchCaseAlignmentRule.swift | 2 +- .../Rules/Style/TrailingWhitespaceRule.swift | 19 +++++++++---------- ...VerticalParameterAlignmentOnCallRule.swift | 4 ++-- .../VerticalParameterAlignmentRule.swift | 2 +- .../Rules/Style/VerticalWhitespaceRule.swift | 2 +- .../Extensions/Request+SwiftLint.swift | 3 +-- .../Configuration+CommandLine.swift | 6 +++--- .../Configuration+FileGraph.swift | 2 +- .../Configuration+FileGraphSubtypes.swift | 2 +- .../Configuration/Configuration+Remote.swift | 2 +- .../Extensions/FileManager+SwiftLint.swift | 2 +- .../LintOrAnalyzeCommand.swift | 5 ++--- Source/SwiftLintFramework/Models/Linter.swift | 2 +- Source/SwiftLintFramework/RulesFilter.swift | 6 +++--- Tests/TestHelpers/TestHelpers.swift | 2 +- 41 files changed, 64 insertions(+), 68 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 97952b8544..5c349312a8 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -37,7 +37,6 @@ disabled_rules: - no_grouping_extension - no_magic_numbers - one_declaration_per_file - - prefer_condition_list - prefer_key_path # Re-enable once we are on Swift 6. - prefer_nimble - prefixed_toplevel_constant diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift index 40bd172131..d1dff3d374 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/DiscouragedObjectLiteralRule.swift @@ -34,11 +34,11 @@ private extension DiscouragedObjectLiteralRule { return } - if !configuration.imageLiteral && identifierText == "imageLiteral" { + if !configuration.imageLiteral, identifierText == "imageLiteral" { return } - if !configuration.colorLiteral && identifierText == "colorLiteral" { + if !configuration.colorLiteral, identifierText == "colorLiteral" { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift index c6c3f6cb64..8ed486f525 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/RedundantVoidReturnRule.swift @@ -73,7 +73,7 @@ struct RedundantVoidReturnRule: Rule { private extension RedundantVoidReturnRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: ReturnClauseSyntax) { - if !configuration.includeClosures && node.parent?.is(ClosureSignatureSyntax.self) == true { + if !configuration.includeClosures, node.parent?.is(ClosureSignatureSyntax.self) == true { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift index 514b8b01fe..281059ba66 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/AccessibilityLabelForImageRule.swift @@ -150,7 +150,7 @@ private extension FunctionCallExprSyntax { } // Check container views with accessibility modifiers - if funcCall.isContainerView() && funcCall.hasAccessibilityModifiersInChain() { + if funcCall.isContainerView(), funcCall.hasAccessibilityModifiersInChain() { return true } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift index 60883d23ad..0ac6e82a4b 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/EmptyXCTestMethodRule.swift @@ -23,7 +23,7 @@ private extension EmptyXCTestMethodRule { } override func visitPost(_ node: FunctionDeclSyntax) { - if (node.modifiers.contains(keyword: .override) || node.isTestMethod) && node.hasEmptyBody { + if node.modifiers.contains(keyword: .override) || node.isTestMethod, node.hasEmptyBody { violations.append(node.funcKeyword.positionAfterSkippingLeadingTrivia) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift index 797f4f2c42..d67dbfb0ae 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/OrphanedDocCommentRule.swift @@ -78,7 +78,7 @@ private extension OrphanedDocCommentRule { switch piece { case .docLineComment(let comment), .docBlockComment(let comment): // These patterns are often used for "file header" style comments - if !comment.hasPrefix("////") && !comment.hasPrefix("/***") { + if !comment.hasPrefix("////"), !comment.hasPrefix("/***") { if isOrphanedDocComment(with: &iterator) { let utf8Length = pieces[..) { - guard startLine > 0 && startLine <= endLine else { return } + guard startLine > 0, startLine <= endLine else { return } for line in startLine...endLine { lines.insert(line) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift index 199d018f2c..b9caa3525e 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/LineLengthRule.swift @@ -72,16 +72,16 @@ private extension LineLengthRule { } // Apply ignore configurations - if configuration.ignoresFunctionDeclarations && functionDeclarationLines.contains(line.index) { + if configuration.ignoresFunctionDeclarations, functionDeclarationLines.contains(line.index) { continue } - if configuration.ignoresComments && commentOnlyLines.contains(line.index) { + if configuration.ignoresComments, commentOnlyLines.contains(line.index) { continue } - if configuration.ignoresInterpolatedStrings && interpolatedStringLines.contains(line.index) { + if configuration.ignoresInterpolatedStrings, interpolatedStringLines.contains(line.index) { continue } - if configuration.ignoresMultilineStrings && multilineStringLines.contains(line.index) { + if configuration.ignoresMultilineStrings, multilineStringLines.contains(line.index) { continue } if configuration.excludedLinesPatterns.contains(where: { diff --git a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift index fbd4a49f86..0f55a1c624 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Metrics/NestingRule.swift @@ -66,7 +66,7 @@ private extension NestingRule { override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind { // if current defines coding keys and we're ignoring coding keys, then skip nesting rule // push another level on and proceed to visit children - if configuration.ignoreCodingKeys && node.definesCodingKeys { + if configuration.ignoreCodingKeys, node.definesCodingKeys { levels.push(levels.lastIsFunction) } else { validate(forFunction: false, triggeringToken: node.enumKeyword) @@ -156,7 +156,7 @@ private extension NestingRule { let targetLevel = forFunction ? configuration.functionLevel : configuration.typeLevel // if parent is function and current is not function types, then skip nesting rule. - if configuration.alwaysAllowOneTypeInFunctions && inFunction && !forFunction { + if configuration.alwaysAllowOneTypeInFunctions, inFunction, !forFunction { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift index 6436937ebc..2b21b5b129 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Performance/EmptyCountRule.swift @@ -148,7 +148,7 @@ private extension EmptyCountRule { private extension ExprSyntax { func countCallPosition(onlyAfterDot: Bool) -> AbsolutePosition? { if let expr = self.as(MemberAccessExprSyntax.self) { - if expr.declName.argumentNames == nil && expr.declName.baseName.tokenKind == .identifier("count") { + if expr.declName.argumentNames == nil, expr.declName.baseName.tokenKind == .identifier("count") { return expr.declName.baseName.positionAfterSkippingLeadingTrivia } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift index 1c4a3f12bc..8316b97980 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OverriddenSuperCallConfiguration.swift @@ -45,7 +45,7 @@ struct OverriddenSuperCallConfiguration: SeverityBasedRuleConfiguration { var resolvedMethodNames: [String] { var names: [String] = [] - if included.contains("*") && !excluded.contains("*") { + if included.contains("*"), !excluded.contains("*") { names += Self.defaultIncluded } names += included.filter { $0 != "*" } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift index cd06da7cf1..a828f7ed92 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/ProhibitedSuperConfiguration.swift @@ -24,7 +24,7 @@ struct ProhibitedSuperConfiguration: SeverityBasedRuleConfiguration { var resolvedMethodNames: [String] { var names = [String]() - if included.contains("*") && !excluded.contains("*") { + if included.contains("*"), !excluded.contains("*") { names += Self.methodNames } names += included.filter { $0 != "*" } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift index be3ad3b7dc..bcdb3330c0 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/AttributesRule.swift @@ -86,7 +86,7 @@ private extension AttributesRule { } let hasMultipleNewlines = node.children(viewMode: .sourceAccurate).enumerated().contains { index, element in - if index > 0 && element.leadingTrivia.hasMultipleNewlines == true { + if index > 0, element.leadingTrivia.hasMultipleNewlines == true { return true } return element.trailingTrivia.hasMultipleNewlines == true @@ -162,7 +162,7 @@ private struct RuleHelper { linesWithAttributes.contains(attributeStartLine) linesWithAttributes.insert(attributeStartLine) if hasViolation { - if attributesWithArgumentsAlwaysOnNewLine && shouldBeOnSameLine { + if attributesWithArgumentsAlwaysOnNewLine, shouldBeOnSameLine { return .argumentsAlwaysOnNewLineViolation } return .violation diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift index aaf84fe061..38ce4e949a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ColonRule.swift @@ -52,19 +52,19 @@ struct ColonRule: SubstitutionCorrectableRule, SourceKitFreeRule { return nil } - if previous.trailingTrivia.isNotEmpty && !previous.trailingTrivia.containsBlockComments() { + if previous.trailingTrivia.isNotEmpty, !previous.trailingTrivia.containsBlockComments() { let start = ByteCount(previous.endPositionBeforeTrailingTrivia) let end = ByteCount(current.endPosition) return ByteRange(location: start, length: end - start) } - if current.trailingTrivia != [.spaces(1)] && !next.leadingTrivia.containsNewlines() { + if current.trailingTrivia != [.spaces(1)], !next.leadingTrivia.containsNewlines() { if case .spaces(1) = current.trailingTrivia.first { return nil } let flexibleRightSpacing = configuration.flexibleRightSpacing || caseStatementPositions.contains(current.position) - if flexibleRightSpacing && current.trailingTrivia.isNotEmpty { + if flexibleRightSpacing, current.trailingTrivia.isNotEmpty { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift index b91f0cb054..752396565d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/CommaRule.swift @@ -105,7 +105,7 @@ struct CommaRule: CorrectableRule, SourceKitFreeRule { if current.tokenKind != .comma { return nil } - if !previous.trailingTrivia.isEmpty && !previous.trailingTrivia.containsBlockComments() { + if !previous.trailingTrivia.isEmpty, !previous.trailingTrivia.containsBlockComments() { let start = ByteCount(previous.endPositionBeforeTrailingTrivia) let end = ByteCount(current.endPosition) let nextIsNewline = next.leadingTrivia.containsNewlines() diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift index ef867230af..d19fb09ddc 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/FileHeaderRule.swift @@ -132,7 +132,7 @@ private extension FileHeaderRule { continue } - if piece.isComment && !piece.isDocComment { + if piece.isComment, !piece.isDocComment { if firstStart == nil { firstStart = pieceStart } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift index 888c9eb01c..77c0d2f5aa 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/IndentationWidthRule.swift @@ -61,7 +61,7 @@ struct IndentationWidthRule: OptInRule { // Determine indentation let indentation: Indentation - if tabCount != 0 && spaceCount != 0 { + if tabCount != 0, spaceCount != 0 { // Catch mixed indentation violations.append( StyleViolation( diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift index 156d6824b6..a31c9c2746 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/MultilineParametersBracketsRule.swift @@ -117,7 +117,7 @@ struct MultilineParametersBracketsRule: OptInRule { let declarationNewlineCount = functionName.countOccurrences(of: "\n") let isMultiline = declarationNewlineCount > parametersNewlineCount - if isMultiline && parameters.isNotEmpty { + if isMultiline, parameters.isNotEmpty { if let openingBracketViolation = openingBracketViolation(parameters: parameters, file: file) { violations.append(openingBracketViolation) } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift index 04e147d210..bbeaa6007d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/NumberSeparatorRule.swift @@ -180,7 +180,7 @@ private extension NumberSeparatorValidator { defer { correctComponents.append(String(char)) } guard char.unicodeScalars.allSatisfy(CharacterSet.decimalDigits.contains) else { continue } - if numerals.isMultiple(of: 3) && numerals > 0 && shouldAddSeparators { + if numerals.isMultiple(of: 3), numerals > 0, shouldAddSeparators { correctComponents.append("_") } numerals += 1 diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift index 66a46c91da..2cbdf8cbbb 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/OperatorUsageWhitespaceRule.swift @@ -175,7 +175,7 @@ private class OperatorUsageWhitespaceVisitor: SyntaxVisitor { let noSpacing = noSpacingBefore || noSpacingAfter let operatorText = operatorToken.text - if noSpacing && allowedNoSpaceOperators.contains(operatorText) { + if noSpacing, allowedNoSpaceOperators.contains(operatorText) { return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift index 971b03f014..a18a97973f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/ReturnArrowWhitespaceRule.swift @@ -87,7 +87,7 @@ private extension TokenSyntax { var end: AbsolutePosition? var correction = " -> " - if previousToken.trailingTrivia != .space && !leadingTrivia.containsNewlines() { + if previousToken.trailingTrivia != .space, !leadingTrivia.containsNewlines() { start = previousToken.endPositionBeforeTrailingTrivia end = endPosition @@ -96,7 +96,7 @@ private extension TokenSyntax { } } - if trailingTrivia != .space && !nextToken.leadingTrivia.containsNewlines() { + if trailingTrivia != .space, !nextToken.leadingTrivia.containsNewlines() { if leadingTrivia.containsNewlines() { start = positionAfterSkippingLeadingTrivia correction = "-> " diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift index 9340ad6e0b..891bb036e3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/StatementPositionRule.swift @@ -199,7 +199,7 @@ private extension StatementPositionRule { } else { newLines = "" } - if !whitespace.hasPrefix("\n") && newLines != "\n" { + if !whitespace.hasPrefix("\n"), newLines != "\n" { whitespace.insert("\n", at: whitespace.startIndex) } contents = contents.bridge().replacingCharacters(in: range2, with: whitespace) diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift index 46592dd7a6..90cc5352a3 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/SwitchCaseAlignmentRule.swift @@ -54,7 +54,7 @@ extension SwitchCaseAlignmentRule { let switchKeywordPosition = node.switchKeyword.positionAfterSkippingLeadingTrivia let switchKeywordLocation = locationConverter.location(for: switchKeywordPosition) - if configuration.ignoreOneLiners && switchKeywordLocation.line == closingBraceLocation.line { + if configuration.ignoreOneLiners, switchKeywordLocation.line == closingBraceLocation.line { return } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift index e7c144da0d..fe0ef2b8a2 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/TrailingWhitespaceRule.swift @@ -48,8 +48,7 @@ private extension TrailingWhitespaceRule { } // Apply `ignoresEmptyLines` configuration - if configuration.ignoresEmptyLines && - line.trimmingCharacters(in: .whitespaces).isEmpty { + if configuration.ignoresEmptyLines, line.trimmingCharacters(in: .whitespaces).isEmpty { continue } @@ -102,7 +101,7 @@ private extension TrailingWhitespaceRule { let pieceStart = currentPos currentPos += piece.sourceLength - if piece.isComment && !piece.isBlockComment { + if piece.isComment, !piece.isBlockComment { let pieceStartLine = locationConverter.location(for: pieceStart).line lineCommentRanges[pieceStartLine, default: []].append(pieceStart.. String { - if trailingWhitespaceInfo.characterCount > 0 && line.count >= trailingWhitespaceInfo.characterCount { + if trailingWhitespaceInfo.characterCount > 0, line.count >= trailingWhitespaceInfo.characterCount { return String(line.prefix(line.count - trailingWhitespaceInfo.characterCount)) } return "" @@ -179,7 +178,7 @@ private extension TrailingWhitespaceRule { // Check if this position falls within any comment range on this line if let ranges = lineCommentRanges[lineNumber] { for range in ranges { - if range.lowerBound <= lastNonWhitespacePos && lastNonWhitespacePos < range.upperBound { + if range.lowerBound <= lastNonWhitespacePos, lastNonWhitespacePos < range.upperBound { return true } } @@ -236,7 +235,7 @@ private extension TrailingWhitespaceRule { var endLine = endLocation.line // If comment ends at column 1, it actually ended on the previous line - if endLocation.column == 1 && endLine > startLine { + if endLocation.column == 1, endLine > startLine { endLine -= 1 } @@ -261,13 +260,13 @@ private extension TrailingWhitespaceRule { let lastNonWhitespacePos = lineStartPos.advanced(by: byteOffsetToLastNonWS) // Check if both first and last non-whitespace positions are within the comment - if firstNonWhitespacePos >= blockCommentStart && lastNonWhitespacePos < blockCommentEnd { + if firstNonWhitespacePos >= blockCommentStart, lastNonWhitespacePos < blockCommentEnd { linesFullyCoveredByBlockComments.insert(lineNum) } } else { // Line is all whitespace - check if it's within the comment bounds let lineEndPos = lineStartPos.advanced(by: lineContent.utf8.count) - if lineStartPos >= blockCommentStart && lineEndPos <= blockCommentEnd { + if lineStartPos >= blockCommentStart, lineEndPos <= blockCommentEnd { linesFullyCoveredByBlockComments.insert(lineNum) } } @@ -294,7 +293,7 @@ private extension String { var charCount = 0 var byteLen = 0 for char in self.reversed() { - if char.isWhitespace && (char == " " || char == "\t") { // Only count spaces and tabs + if char.isWhitespace, char == " " || char == "\t" { // Only count spaces and tabs charCount += 1 byteLen += char.utf8.count } else { diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift index b3651e765c..589bb32ac1 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentOnCallRule.swift @@ -150,13 +150,13 @@ private extension VerticalParameterAlignmentOnCallRule { } let (firstVisit, _) = visitedLines.insert(location.line) - guard location.column != firstArgumentLocation.column && firstVisit else { + guard location.column != firstArgumentLocation.column, firstVisit else { return nil } // if this is the first element on a new line after a closure with multiple lines, // we reset the reference position - if previousArgumentWasMultiline && firstVisit { + if previousArgumentWasMultiline, firstVisit { firstArgumentLocation = location return nil } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRule.swift index 84c7020655..9f8d41506f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalParameterAlignmentRule.swift @@ -40,7 +40,7 @@ private extension VerticalParameterAlignmentRule { var violations: [AbsolutePosition] = [] for (index, paramLoc) in paramLocations.enumerated() where index > 0 && paramLoc.line > firstParamLoc.line { let previousParamLoc = paramLocations[index - 1] - if previousParamLoc.line < paramLoc.line && firstParamLoc.column != paramLoc.column { + if previousParamLoc.line < paramLoc.line, firstParamLoc.column != paramLoc.column { violations.append(paramLoc.position) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift index 956b5a28a0..23cc943260 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Style/VerticalWhitespaceRule.swift @@ -70,7 +70,7 @@ private extension VerticalWhitespaceRule { func process(_ count: Int, _ offset: Int) { for _ in 0..<(count + firstTokenAdditionalNewlines) { - if consecutiveNewlines > configuration.maxEmptyLines && violationPosition == nil { + if consecutiveNewlines > configuration.maxEmptyLines, violationPosition == nil { violationPosition = currentPosition } consecutiveNewlines += 1 diff --git a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift index 467723d54b..d012b9adba 100644 --- a/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift +++ b/Source/SwiftLintCore/Extensions/Request+SwiftLint.swift @@ -21,8 +21,7 @@ public extension Request { // Check if the current rule is a SourceKitFreeRule // Skip check for ConditionallySourceKitFree rules since we can't determine // at the type level if they're effectively SourceKit-free - if ruleType is any SourceKitFreeRule.Type && - !(ruleType is any ConditionallySourceKitFree.Type) { + if ruleType is any SourceKitFreeRule.Type, !(ruleType is any ConditionallySourceKitFree.Type) { queuedFatalError(""" '\(ruleID)' is a SourceKitFreeRule and should not be making requests to SourceKit. """) diff --git a/Source/SwiftLintFramework/Configuration+CommandLine.swift b/Source/SwiftLintFramework/Configuration+CommandLine.swift index ac494f9b18..06549f9bbe 100644 --- a/Source/SwiftLintFramework/Configuration+CommandLine.swift +++ b/Source/SwiftLintFramework/Configuration+CommandLine.swift @@ -105,7 +105,7 @@ extension Configuration { private func groupFiles(_ files: [SwiftLintFile], visitor: LintableFilesVisitor) throws -> [Configuration: [SwiftLintFile]] { - if files.isEmpty && !visitor.allowZeroLintableFiles { + if files.isEmpty, !visitor.allowZeroLintableFiles { throw SwiftLintError.usageError( description: "No lintable files found at paths: '\(visitor.options.paths.joined(separator: ", "))'" ) @@ -166,12 +166,12 @@ extension Configuration { let counter = CounterActor() let total = linters.filter(\.isCollecting).count let progress = ProgressBar(count: total) - if visitor.options.progress && total > 0 { + if visitor.options.progress, total > 0 { await progress.initialize() } let collect = { (linter: Linter) -> CollectedLinter? in let skipFile = visitor.shouldSkipFile(atPath: linter.file.path) - if !visitor.options.quiet && linter.isCollecting { + if !visitor.options.quiet, linter.isCollecting { if visitor.options.progress { await progress.printNext() } else if let filePath = linter.file.path { diff --git a/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift b/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift index 464553659e..e15d0f5fa6 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+FileGraph.swift @@ -146,7 +146,7 @@ package extension Configuration { // Local vertices are allowed to have local / remote references // Remote vertices are only allowed to have remote references - if vertex.originatesFromRemote && !referencedVertex.originatesFromRemote { + if vertex.originatesFromRemote, !referencedVertex.originatesFromRemote { throw Issue.genericWarning("Remote configs are not allowed to reference local configs.") } let existingVertex = findPossiblyExistingVertex(sameAs: referencedVertex) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift b/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift index f1a8283cf7..584a78c27b 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+FileGraphSubtypes.swift @@ -71,7 +71,7 @@ internal extension Configuration.FileGraph { } private func read(at path: String) throws -> String { - guard !path.isEmpty && FileManager.default.fileExists(atPath: path) else { + guard !path.isEmpty, FileManager.default.fileExists(atPath: path) else { throw isInitialVertex ? Issue.initialFileNotFound(path: path) : Issue.fileNotFound(path: path) diff --git a/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift b/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift index e8eec05c86..bd27bce998 100644 --- a/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift +++ b/Source/SwiftLintFramework/Configuration/Configuration+Remote.swift @@ -240,7 +240,7 @@ internal extension Configuration.FileGraph.FilePath { // Delete all cache folders except for the current version's folder let directoryWithoutVersionNum = directory.components(separatedBy: "/").dropLast().joined(separator: "/") try (try FileManager.default.subpathsOfDirectory(atPath: directoryWithoutVersionNum)).forEach { - if !$0.contains("/") && $0 != Configuration.FileGraph.FilePath.remoteCacheVersionNumber { + if !$0.contains("/"), $0 != Configuration.FileGraph.FilePath.remoteCacheVersionNumber { try FileManager.default.removeItem(atPath: $0.bridge().absolutePathRepresentation(rootDirectory: directoryWithoutVersionNum) ) diff --git a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift index c5aad909a5..f76ca676f3 100644 --- a/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift +++ b/Source/SwiftLintFramework/Extensions/FileManager+SwiftLint.swift @@ -35,7 +35,7 @@ extension FileManager: LintableFileManager { .standardizingPath // if path is a file, it won't be returned in `enumerator(atPath:)` - if absolutePath.bridge().isSwiftFile() && absolutePath.isFile { + if absolutePath.bridge().isSwiftFile(), absolutePath.isFile { return [absolutePath] } diff --git a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift index be16658172..4af32083a4 100644 --- a/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift +++ b/Source/SwiftLintFramework/LintOrAnalyzeCommand.swift @@ -203,8 +203,7 @@ package struct LintOrAnalyzeCommand { ) throws -> Int { let options = builder.options let configuration = builder.configuration - if isWarningThresholdBroken(configuration: configuration, violations: builder.violations) - && !options.lenient { + if isWarningThresholdBroken(configuration: configuration, violations: builder.violations), !options.lenient { builder.violations.append( createThresholdViolation(threshold: configuration.warningThreshold!) ) @@ -325,7 +324,7 @@ package struct LintOrAnalyzeCommand { } let corrections = linter.correct(using: storage) - if !corrections.isEmpty && !options.quiet { + if !corrections.isEmpty, !options.quiet { if options.useSTDIN { queuedPrint(linter.file.contents) } else { diff --git a/Source/SwiftLintFramework/Models/Linter.swift b/Source/SwiftLintFramework/Models/Linter.swift index d25adf42a5..525d25b18c 100644 --- a/Source/SwiftLintFramework/Models/Linter.swift +++ b/Source/SwiftLintFramework/Models/Linter.swift @@ -80,7 +80,7 @@ private extension Rule { // Only check sourcekitdFailed if the rule requires SourceKit. // This avoids triggering SourceKit initialization for SourceKit-free rules. - if requiresSourceKit && file.sourcekitdFailed { + if requiresSourceKit, file.sourcekitdFailed { warnSourceKitFailedOnce() return false } diff --git a/Source/SwiftLintFramework/RulesFilter.swift b/Source/SwiftLintFramework/RulesFilter.swift index 8a5d1d250e..d0cac706d4 100644 --- a/Source/SwiftLintFramework/RulesFilter.swift +++ b/Source/SwiftLintFramework/RulesFilter.swift @@ -30,13 +30,13 @@ package final class RulesFilter { } let isRuleEnabled = enabledRule != nil - if excludingOptions.contains(.enabled) && isRuleEnabled { + if excludingOptions.contains(.enabled), isRuleEnabled { return nil } - if excludingOptions.contains(.disabled) && !isRuleEnabled { + if excludingOptions.contains(.disabled), !isRuleEnabled { return nil } - if excludingOptions.contains(.uncorrectable) && !(ruleType is any CorrectableRule.Type) { + if excludingOptions.contains(.uncorrectable), !(ruleType is any CorrectableRule.Type) { return nil } diff --git a/Tests/TestHelpers/TestHelpers.swift b/Tests/TestHelpers/TestHelpers.swift index 79fc255681..10cdc8b23d 100644 --- a/Tests/TestHelpers/TestHelpers.swift +++ b/Tests/TestHelpers/TestHelpers.swift @@ -275,7 +275,7 @@ private func testCorrection(_ correction: (Example, Example), } config.assertCorrection(correction.0, expected: correction.1) - if testMultiByteOffsets && correction.0.testMultiByteOffsets { + if testMultiByteOffsets, correction.0.testMultiByteOffsets { config.assertCorrection(addEmoji(correction.0), expected: addEmoji(correction.1)) } } From 532e6617edd73dc7a018ad4022fb16c151d14299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Mon, 14 Jul 2025 21:21:11 +0200 Subject: [PATCH 68/80] Disable background indexing in Swift extension https://github.com/swiftlang/sourcekit-lsp/issues/2205 --- .sourcekit-lsp/config.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .sourcekit-lsp/config.json diff --git a/.sourcekit-lsp/config.json b/.sourcekit-lsp/config.json new file mode 100644 index 0000000000..311a84771b --- /dev/null +++ b/.sourcekit-lsp/config.json @@ -0,0 +1,3 @@ +{ + "backgroundIndexing": false +} From 69c9e29833953c000e3ce5d6479f8749f085d857 Mon Sep 17 00:00:00 2001 From: Erik Kerber Date: Wed, 30 Jul 2025 02:36:11 -0500 Subject: [PATCH 69/80] Add Sendable conformance to Rule.Type for building with Swift 6 (#6169) --- CHANGELOG.md | 4 ++++ Source/SwiftLintCore/Models/AccessControlLevel.swift | 2 +- Source/SwiftLintCore/Models/RuleParameter.swift | 2 +- Source/SwiftLintCore/Protocols/Rule.swift | 2 +- Source/SwiftLintCore/Protocols/RuleConfiguration.swift | 2 +- .../SwiftLintCore/RuleConfigurations/RegexConfiguration.swift | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 694500781c..77731ad487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ ### Enhancements +* Add Sendable conformance to Rule.Type for building with Swift 6. + [erikkerber](https://github.com/erikkerber) + [#issue_number](https://github.com/realm/SwiftLint/issues/issue_number) + * Fix false positives for `Actor`-conforming delegate protocols in the `class_delegate_protocol` rule. [imsonalbajaj](https://github.com/imsonalbajaj) diff --git a/Source/SwiftLintCore/Models/AccessControlLevel.swift b/Source/SwiftLintCore/Models/AccessControlLevel.swift index 11b1729a76..6570f916d7 100644 --- a/Source/SwiftLintCore/Models/AccessControlLevel.swift +++ b/Source/SwiftLintCore/Models/AccessControlLevel.swift @@ -1,7 +1,7 @@ /// The accessibility of a Swift source declaration. /// /// - SeeAlso: https://github.com/apple/swift/blob/main/docs/AccessControl.md -public enum AccessControlLevel: String, CustomStringConvertible { +public enum AccessControlLevel: String, CustomStringConvertible, Sendable { /// Accessible by the declaration's immediate lexical scope. case `private` = "source.lang.swift.accessibility.private" /// Accessible by the declaration's same file. diff --git a/Source/SwiftLintCore/Models/RuleParameter.swift b/Source/SwiftLintCore/Models/RuleParameter.swift index 356395baaa..9d09264489 100644 --- a/Source/SwiftLintCore/Models/RuleParameter.swift +++ b/Source/SwiftLintCore/Models/RuleParameter.swift @@ -1,5 +1,5 @@ /// A configuration parameter for rules. -public struct RuleParameter: Equatable { +public struct RuleParameter: Equatable, Sendable where T: Sendable { /// The severity that should be assigned to the violation of this parameter's value is met. public let severity: ViolationSeverity /// The value to configure the rule. diff --git a/Source/SwiftLintCore/Protocols/Rule.swift b/Source/SwiftLintCore/Protocols/Rule.swift index c81f2282e7..3e31915f28 100644 --- a/Source/SwiftLintCore/Protocols/Rule.swift +++ b/Source/SwiftLintCore/Protocols/Rule.swift @@ -2,7 +2,7 @@ import Foundation import SourceKittenFramework /// An executable value that can identify issues (violations) in Swift source code. -public protocol Rule { +public protocol Rule: Sendable { /// The type of the configuration used to configure this rule. associatedtype ConfigurationType: RuleConfiguration diff --git a/Source/SwiftLintCore/Protocols/RuleConfiguration.swift b/Source/SwiftLintCore/Protocols/RuleConfiguration.swift index c750417df4..ce97982aba 100644 --- a/Source/SwiftLintCore/Protocols/RuleConfiguration.swift +++ b/Source/SwiftLintCore/Protocols/RuleConfiguration.swift @@ -1,5 +1,5 @@ /// A configuration value for a rule to allow users to modify its behavior. -public protocol RuleConfiguration: Equatable { +public protocol RuleConfiguration: Equatable, Sendable { /// The type of the rule that's using this configuration. associatedtype Parent: Rule diff --git a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift index 8f5590a41a..83e6529d61 100644 --- a/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift +++ b/Source/SwiftLintCore/RuleConfigurations/RegexConfiguration.swift @@ -1,5 +1,5 @@ import Foundation -import SourceKittenFramework +@preconcurrency import SourceKittenFramework /// A rule configuration used for defining custom rules in yaml. public struct RegexConfiguration: SeverityBasedRuleConfiguration, Hashable, From aac32899c39819cd111c040a849f638a48bfc330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Wed, 30 Jul 2025 14:57:35 +0200 Subject: [PATCH 70/80] Exclude variable from concurrency analysis (#6170) The variable is only used in `setUp` and `tearDown` and so access is not concurrent. --- Tests/FrameworkTests/BaselineTests.swift | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Tests/FrameworkTests/BaselineTests.swift b/Tests/FrameworkTests/BaselineTests.swift index f341d0012d..a97585c827 100644 --- a/Tests/FrameworkTests/BaselineTests.swift +++ b/Tests/FrameworkTests/BaselineTests.swift @@ -47,10 +47,6 @@ final class BaselineTests: XCTestCase { DirectReturnRule.description, ] - private actor CurrentDirectoryHolder { - static var currentDirectoryPath: String? - } - private static func violations(for filePath: String?) -> [StyleViolation] { ruleDescriptions.violations(for: filePath) } @@ -59,17 +55,16 @@ final class BaselineTests: XCTestCase { Baseline(violations: ruleDescriptions.violations(for: filePath)) } + private nonisolated(unsafe) static var currentDirectoryPath: String? + override static func setUp() { super.setUp() - CurrentDirectoryHolder.currentDirectoryPath = FileManager.default.currentDirectoryPath + currentDirectoryPath = FileManager.default.currentDirectoryPath XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(temporaryDirectoryPath)) } override static func tearDown() { - if let currentDirectoryPath = CurrentDirectoryHolder.currentDirectoryPath { - XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(currentDirectoryPath)) - CurrentDirectoryHolder.currentDirectoryPath = nil - } + XCTAssertTrue(FileManager.default.changeCurrentDirectoryPath(currentDirectoryPath!)) super.tearDown() } From 00f2d216526c6203c563c56bbdeda34ece188d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 1 Aug 2025 16:10:13 +0200 Subject: [PATCH 71/80] Add config schema --- .sourcekit-lsp/config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.sourcekit-lsp/config.json b/.sourcekit-lsp/config.json index 311a84771b..e93a7dc4f6 100644 --- a/.sourcekit-lsp/config.json +++ b/.sourcekit-lsp/config.json @@ -1,3 +1,4 @@ { + "$schema": "https://raw.githubusercontent.com/swiftlang/sourcekit-lsp/refs/heads/release/6.1/config.schema.json", "backgroundIndexing": false } From deb3678e6fe110f75df2a6b3b25d181a0fb863be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 1 Aug 2025 16:22:16 +0200 Subject: [PATCH 72/80] Add setup steps for coding agent --- .github/workflows/copilot-setup-steps.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000000..feb9028e1c --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,21 @@ +name: "Copilot Setup Steps" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-24.04 + container: swift:6.1-noble + permissions: + contents: none + steps: + - name: "Check Swift version" + run: swift --version From 5738a6138e2bcee8a8141f1dddd6fafd48d34eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 1 Aug 2025 16:31:39 +0200 Subject: [PATCH 73/80] Revert "Add setup steps for coding agent" This reverts commit deb3678e6fe110f75df2a6b3b25d181a0fb863be. --- .github/workflows/copilot-setup-steps.yml | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml deleted file mode 100644 index feb9028e1c..0000000000 --- a/.github/workflows/copilot-setup-steps.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: "Copilot Setup Steps" - -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -jobs: - # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. - copilot-setup-steps: - runs-on: ubuntu-24.04 - container: swift:6.1-noble - permissions: - contents: none - steps: - - name: "Check Swift version" - run: swift --version From 7395ead738fdd66ec30d03ea314564dc62780f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danny=20M=C3=B6sch?= Date: Fri, 1 Aug 2025 17:39:49 +0200 Subject: [PATCH 74/80] Add basic Copilot instructions --- .github/copilot-instructions.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..cd6188a93f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +You are working on SwiftLint, a linter for Swift code that enforces coding style and conventions. It helps maintain a clean and consistent codebase by identifying and reporting issues in Swift files. It can even automatically fix some of these issues. + +Linting rules are defined in `Source/SwiftLintBuiltInRules/Rules`. If someone mentions a rule by its identifier that is in "snake_case" (e.g., `rule_name`), you can usually find the rule's implementation file named "UpperCamelCaseRule.swift" (e.g., `Rule.swift`) in one of the sub-folders depending on the rule's kind. Specific configurations for rules are located in the `RuleConfigurations` folder, which contains files named as `Configuration.swift` (e.g., `IdentifierNameConfiguration.swift`). + +User-facing changes must be documented in the `CHANGELOG.md` file, which is organized by version. New entries always go into the "Main" section. They give credit to the person who has made the change and they reference the issue which has been fixed by the change. + +All changes need to pass `swift test` as well as running SwiftLint on itself. This is done by running `swift run swiftlint` in the root directory of the project. From 8bb69b064a96ef12c3d79bd153da7a1fa4a52089 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 Aug 2025 17:49:58 -0400 Subject: [PATCH 75/80] Add `include_variables` option to `non_optional_string_data_conversion` rule (#6172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Danny Mösch --- CHANGELOG.md | 6 ++++ .../NonOptionalStringDataConversionRule.swift | 30 +++++++++++++++---- ...nalStringDataConversionConfiguration.swift | 12 ++++++++ .../default_rule_configurations.yml | 1 + 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 77731ad487..c5d5109d02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ ### Enhancements +* Add `include_variables` configuration option to `non_optional_string_data_conversion` rule. + When enabled, the rule will trigger on variables, properties, and function calls in addition + to string literals. Defaults to `false` for backward compatibility. + [SimplyDanny](https://github.com/SimplyDanny) + [#6094](https://github.com/realm/SwiftLint/issues/6094) + * Add Sendable conformance to Rule.Type for building with Swift 6. [erikkerber](https://github.com/erikkerber) [#issue_number](https://github.com/realm/SwiftLint/issues/issue_number) diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift index 5141717370..bb690ef813 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Lint/NonOptionalStringDataConversionRule.swift @@ -1,18 +1,35 @@ +import SwiftLintCore import SwiftSyntax @SwiftSyntaxRule struct NonOptionalStringDataConversionRule: Rule { - var configuration = SeverityConfiguration(.warning) + var configuration = NonOptionalStringDataConversionConfiguration() + + private static let variablesIncluded = ["include_variables": true] + static let description = RuleDescription( identifier: "non_optional_string_data_conversion", name: "Non-optional String -> Data Conversion", description: "Prefer non-optional `Data(_:)` initializer when converting `String` to `Data`", kind: .lint, nonTriggeringExamples: [ - Example("Data(\"foo\".utf8)") + Example("Data(\"foo\".utf8)"), + Example("Data(string.utf8)"), + Example("\"foo\".data(using: .ascii)"), + Example("string.data(using: .unicode)"), + Example("Data(\"foo\".utf8)", configuration: variablesIncluded), + Example("Data(string.utf8)", configuration: variablesIncluded), + Example("\"foo\".data(using: .ascii)", configuration: variablesIncluded), + Example("string.data(using: .unicode)", configuration: variablesIncluded), ], triggeringExamples: [ - Example("\"foo\".data(using: .utf8)") + Example("↓\"foo\".data(using: .utf8)"), + Example("↓\"foo\".data(using: .utf8)", configuration: variablesIncluded), + Example("↓string.data(using: .utf8)", configuration: variablesIncluded), + Example("↓property.data(using: .utf8)", configuration: variablesIncluded), + Example("↓obj.property.data(using: .utf8)", configuration: variablesIncluded), + Example("↓getString().data(using: .utf8)", configuration: variablesIncluded), + Example("↓getValue()?.data(using: .utf8)", configuration: variablesIncluded), ] ) } @@ -20,12 +37,13 @@ struct NonOptionalStringDataConversionRule: Rule { private extension NonOptionalStringDataConversionRule { final class Visitor: ViolationsSyntaxVisitor { override func visitPost(_ node: MemberAccessExprSyntax) { - if node.base?.is(StringLiteralExprSyntax.self) == true, - node.declName.baseName.text == "data", + if node.declName.baseName.text == "data", let parent = node.parent?.as(FunctionCallExprSyntax.self), let argument = parent.arguments.onlyElement, argument.label?.text == "using", - argument.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true { + argument.expression.as(MemberAccessExprSyntax.self)?.isUTF8 == true, + let base = node.base, + base.is(StringLiteralExprSyntax.self) || configuration.includeVariables { violations.append(node.positionAfterSkippingLeadingTrivia) } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift new file mode 100644 index 0000000000..ef21f1ac39 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/NonOptionalStringDataConversionConfiguration.swift @@ -0,0 +1,12 @@ +import SwiftLintCore + +@AutoConfigParser +struct NonOptionalStringDataConversionConfiguration: SeverityBasedRuleConfiguration { + // swiftlint:disable:previous type_name + typealias Parent = NonOptionalStringDataConversionRule + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + @ConfigurationElement(key: "include_variables") + private(set) var includeVariables = false +} diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index aaeffb97b6..094ad89c07 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -737,6 +737,7 @@ no_space_in_method_call: correctable: true non_optional_string_data_conversion: severity: warning + include_variables: false meta: opt-in: false correctable: false From 201b90acbaf514487b3ed67b4efcb1a8290e6b58 Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Mon, 19 May 2025 07:58:18 +0200 Subject: [PATCH 76/80] Initial implementation of allowed_types option for `one-declaration-per-file` rule --- .../Idiomatic/OneDeclarationPerFileRule.swift | 29 ++++++++++++++++--- .../OneDeclarationPerFileConfiguration.swift | 26 +++++++++++++++++ .../default_rule_configurations.yml | 1 + 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index 784a4e3456..a220a4030a 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -2,8 +2,7 @@ import SwiftSyntax @SwiftSyntaxRule(optIn: true) struct OneDeclarationPerFileRule: Rule { - var configuration = SeverityConfiguration(.warning) - + var configuration = OneDeclarationPerFileConfiguration() static let description = RuleDescription( identifier: "one_declaration_per_file", name: "One Declaration per File", @@ -22,6 +21,18 @@ struct OneDeclarationPerFileRule: Rule { struct N {} } """), + Example(""" + enum Foo { + } + struct Bar { + } + """, + configuration: ["allowed_types": ["enum", "struct"]]), + Example(""" + struct Foo {} + struct Bar {} + """, + configuration: ["allowed_types": ["struct"]]), ], triggeringExamples: [ Example(""" @@ -36,14 +47,24 @@ struct OneDeclarationPerFileRule: Rule { struct Foo {} ↓struct Bar {} """), + Example(""" + struct Foo {} + ↓enum Bar {} + """, + configuration: ["allowed_types": ["protocol"]]), ] ) } private extension OneDeclarationPerFileRule { final class Visitor: ViolationsSyntaxVisitor { + private let allowedTypes: Set private var declarationVisited = false override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + override init(configuration: OneDeclarationPerFileConfiguration, file: SwiftLintFile) { + allowedTypes = Set(configuration.enabledTypes.map(\.rawValue)) + super.init(configuration: configuration, file: file) + } override func visitPost(_ node: ActorDeclSyntax) { appendViolationIfNeeded(node: node.actorKeyword) @@ -66,10 +87,10 @@ private extension OneDeclarationPerFileRule { } func appendViolationIfNeeded(node: TokenSyntax) { - if declarationVisited { + defer { declarationVisited = true } + if declarationVisited && !allowedTypes.contains(node.text) { violations.append(node.positionAfterSkippingLeadingTrivia) } - declarationVisited = true } } } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift new file mode 100644 index 0000000000..93951a8d03 --- /dev/null +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift @@ -0,0 +1,26 @@ +import SwiftLintCore + +@AutoConfigParser +struct OneDeclarationPerFileConfiguration: SeverityBasedRuleConfiguration { + typealias Parent = OneDeclarationPerFileRule + + @AcceptableByConfigurationElement + enum AllowedType: String, CaseIterable { + case `actor` + case `class` + case `enum` + case `protocol` + case `struct` + static let all = Set(allCases) + } + + @ConfigurationElement(key: "severity") + private(set) var severityConfiguration = SeverityConfiguration(.warning) + + @ConfigurationElement(key: "allowed_types") + private(set) var allowedTypes: [AllowedType] = [] + + var enabledTypes: Set { + Set(self.allowedTypes) + } +} diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 094ad89c07..06e91aa531 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -791,6 +791,7 @@ one_declaration_per_file: meta: opt-in: true correctable: false + allowed_types: [] opening_brace: severity: warning ignore_multiline_type_headers: false From 5e8e92a468fbbca82909480366b97e63b183c403 Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Tue, 20 May 2025 21:11:48 +0200 Subject: [PATCH 77/80] Add unit test, add changelog entry --- CHANGELOG.md | 3 ++ .../Idiomatic/OneDeclarationPerFileRule.swift | 1 + .../OneDeclarationPerFileConfiguration.swift | 1 + ...eDeclarationPerFileConfigurationTest.swift | 39 +++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d5109d02..29ecb8b627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,9 @@ * Support deinitializers and subscripts in `function_body_length` rule. [SimplyDanny](https://github.com/SimplyDanny) +* Add new `allowed_types` option to the `one_declaration_per_file` rule. + [Alfons Hoogervorst](https://github.com/snofla) + [#6072](https://github.com/realm/SwiftLint/issues/6072) ### Bug Fixes diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index a220a4030a..8deffc644d 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -61,6 +61,7 @@ private extension OneDeclarationPerFileRule { private let allowedTypes: Set private var declarationVisited = false override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } + override init(configuration: OneDeclarationPerFileConfiguration, file: SwiftLintFile) { allowedTypes = Set(configuration.enabledTypes.map(\.rawValue)) super.init(configuration: configuration, file: file) diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift index 93951a8d03..b90df8fc2f 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift @@ -11,6 +11,7 @@ struct OneDeclarationPerFileConfiguration: SeverityBasedRuleConfiguration { case `enum` case `protocol` case `struct` + static let all = Set(allCases) } diff --git a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift new file mode 100644 index 0000000000..89769decbb --- /dev/null +++ b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift @@ -0,0 +1,39 @@ +@testable import SwiftLintBuiltInRules +import TestHelpers +import XCTest + +final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { + func testOneDeclarationPerFileConfigurationCheckSettingAllowedTypes() throws { + let initial: [OneDeclarationPerFileConfiguration.AllowedType] = [ + .actor, .class + ] + let config = OneDeclarationPerFileConfiguration(severityConfiguration: .warning, allowedTypes: initial) + XCTAssertEqual(Set(initial), config.enabledTypes) + } + + func testOneDeclarationPerFileConfigurationGoodConfig() throws { + let allowedTypes = OneDeclarationPerFileConfiguration.AllowedType.all + let allowedTypesString: [String] = allowedTypes.map(\.rawValue) + .sorted() + let goodConfig: [String: Any] = [ + "severity": "error", + "allowed_types": allowedTypesString + ] + var configuration = OneDeclarationPerFileConfiguration() + try configuration.apply(configuration: goodConfig) + XCTAssertEqual(configuration.severityConfiguration.severity, .error) + XCTAssertEqual(configuration.enabledTypes, allowedTypes) + } + + func testOneDeclarationPerFileConfigurationBadConfigWrongTypes() throws { + let badConfig: [String: Any] = [ + "severity": "error", + "allowed_types": ["clas"] + ] + var configuration = OneDeclarationPerFileConfiguration() + checkError(Issue.invalidConfiguration(ruleID: OneDeclarationPerFileRule.identifier)) { + try configuration.apply(configuration: badConfig) + } + XCTAssert(configuration.enabledTypes == []) + } +} From 0b1200359ffbb68813ac1e36a31a26af3aa7f73f Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Wed, 21 May 2025 05:31:54 +0200 Subject: [PATCH 78/80] Minor fixes --- .../OneDeclarationPerFileConfigurationTest.swift | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift index 89769decbb..c02b8190d8 100644 --- a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift +++ b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift @@ -17,7 +17,7 @@ final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { .sorted() let goodConfig: [String: Any] = [ "severity": "error", - "allowed_types": allowedTypesString + "allowed_types": allowedTypesString, ] var configuration = OneDeclarationPerFileConfiguration() try configuration.apply(configuration: goodConfig) @@ -28,12 +28,11 @@ final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { func testOneDeclarationPerFileConfigurationBadConfigWrongTypes() throws { let badConfig: [String: Any] = [ "severity": "error", - "allowed_types": ["clas"] + "allowed_types": ["clas"], ] var configuration = OneDeclarationPerFileConfiguration() checkError(Issue.invalidConfiguration(ruleID: OneDeclarationPerFileRule.identifier)) { try configuration.apply(configuration: badConfig) } - XCTAssert(configuration.enabledTypes == []) - } + } } From fe356c70fe125c4995c51a2c6a001f460e90132f Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Wed, 21 May 2025 19:22:35 +0200 Subject: [PATCH 79/80] Fix default rules --- Tests/IntegrationTests/default_rule_configurations.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 06e91aa531..f2ddd54d8e 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -788,10 +788,10 @@ object_literal: correctable: false one_declaration_per_file: severity: warning + allowed_types: [] meta: opt-in: true correctable: false - allowed_types: [] opening_brace: severity: warning ignore_multiline_type_headers: false From 6475db629a21f69ab5a7ecf85872b56531262294 Mon Sep 17 00:00:00 2001 From: Alfons Hoogervorst Date: Sun, 3 Aug 2025 19:45:28 +0200 Subject: [PATCH 80/80] Rename type spec config setting See: https://github.com/realm/SwiftLint/pull/6082#issuecomment-3033450451 --- .../Idiomatic/OneDeclarationPerFileRule.swift | 8 ++++---- .../OneDeclarationPerFileConfiguration.swift | 11 +++++------ ...neDeclarationPerFileConfigurationTest.swift | 18 +++++++++--------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift index 8deffc644d..da16051f38 100644 --- a/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift +++ b/Source/SwiftLintBuiltInRules/Rules/Idiomatic/OneDeclarationPerFileRule.swift @@ -27,12 +27,12 @@ struct OneDeclarationPerFileRule: Rule { struct Bar { } """, - configuration: ["allowed_types": ["enum", "struct"]]), + configuration: ["ignored_types": ["enum", "struct"]]), Example(""" struct Foo {} struct Bar {} """, - configuration: ["allowed_types": ["struct"]]), + configuration: ["ignored_types": ["struct"]]), ], triggeringExamples: [ Example(""" @@ -51,7 +51,7 @@ struct OneDeclarationPerFileRule: Rule { struct Foo {} ↓enum Bar {} """, - configuration: ["allowed_types": ["protocol"]]), + configuration: ["ignored_types": ["protocol"]]), ] ) } @@ -63,7 +63,7 @@ private extension OneDeclarationPerFileRule { override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { .all } override init(configuration: OneDeclarationPerFileConfiguration, file: SwiftLintFile) { - allowedTypes = Set(configuration.enabledTypes.map(\.rawValue)) + allowedTypes = Set(configuration.allowedTypes.map(\.rawValue)) super.init(configuration: configuration, file: file) } diff --git a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift index b90df8fc2f..d9d18f6632 100644 --- a/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift +++ b/Source/SwiftLintBuiltInRules/Rules/RuleConfigurations/OneDeclarationPerFileConfiguration.swift @@ -5,23 +5,22 @@ struct OneDeclarationPerFileConfiguration: SeverityBasedRuleConfiguration { typealias Parent = OneDeclarationPerFileRule @AcceptableByConfigurationElement - enum AllowedType: String, CaseIterable { + enum IgnoredType: String, CaseIterable { case `actor` case `class` case `enum` case `protocol` case `struct` - static let all = Set(allCases) } @ConfigurationElement(key: "severity") private(set) var severityConfiguration = SeverityConfiguration(.warning) - @ConfigurationElement(key: "allowed_types") - private(set) var allowedTypes: [AllowedType] = [] + @ConfigurationElement(key: "ignored_types") + private(set) var ignoredTypes: [IgnoredType] = [] - var enabledTypes: Set { - Set(self.allowedTypes) + var allowedTypes: Set { + Set(self.ignoredTypes) } } diff --git a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift index c02b8190d8..54025ec317 100644 --- a/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift +++ b/Tests/BuiltInRulesTests/OneDeclarationPerFileConfigurationTest.swift @@ -3,32 +3,32 @@ import TestHelpers import XCTest final class OneDeclarationPerFileConfigurationTest: SwiftLintTestCase { - func testOneDeclarationPerFileConfigurationCheckSettingAllowedTypes() throws { - let initial: [OneDeclarationPerFileConfiguration.AllowedType] = [ + func testOneDeclarationPerFileConfigurationCheckSettingIgnoredTypes() throws { + let initial: [OneDeclarationPerFileConfiguration.IgnoredType] = [ .actor, .class ] - let config = OneDeclarationPerFileConfiguration(severityConfiguration: .warning, allowedTypes: initial) - XCTAssertEqual(Set(initial), config.enabledTypes) + let configuration = OneDeclarationPerFileConfiguration(severityConfiguration: .warning, ignoredTypes: initial) + XCTAssertEqual(Set(initial), configuration.allowedTypes) } func testOneDeclarationPerFileConfigurationGoodConfig() throws { - let allowedTypes = OneDeclarationPerFileConfiguration.AllowedType.all - let allowedTypesString: [String] = allowedTypes.map(\.rawValue) + let ignoredTypes = OneDeclarationPerFileConfiguration.IgnoredType.all + let ignoredTypesString: [String] = ignoredTypes.map(\.rawValue) .sorted() let goodConfig: [String: Any] = [ "severity": "error", - "allowed_types": allowedTypesString, + "ignored_types": ignoredTypesString, ] var configuration = OneDeclarationPerFileConfiguration() try configuration.apply(configuration: goodConfig) XCTAssertEqual(configuration.severityConfiguration.severity, .error) - XCTAssertEqual(configuration.enabledTypes, allowedTypes) + XCTAssertEqual(configuration.allowedTypes, ignoredTypes) } func testOneDeclarationPerFileConfigurationBadConfigWrongTypes() throws { let badConfig: [String: Any] = [ "severity": "error", - "allowed_types": ["clas"], + "ignored_types": ["clas"], ] var configuration = OneDeclarationPerFileConfiguration() checkError(Issue.invalidConfiguration(ruleID: OneDeclarationPerFileRule.identifier)) {