diff --git a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift index 075d9b8fd2..2527ee1d79 100644 --- a/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift +++ b/Source/SwiftLintBuiltInRules/Models/BuiltInRules.swift @@ -224,7 +224,6 @@ public let builtInRules: [any Rule.Type] = [ UnneededSynthesizedInitializerRule.self, UnownedVariableCaptureRule.self, UntypedErrorInCatchRule.self, - UnusedCaptureListRule.self, UnusedClosureParameterRule.self, UnusedControlFlowLabelRule.self, UnusedDeclarationRule.self, diff --git a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift b/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift deleted file mode 100644 index 445aba6edf..0000000000 --- a/Source/SwiftLintBuiltInRules/Rules/Lint/UnusedCaptureListRule.swift +++ /dev/null @@ -1,233 +0,0 @@ -import SwiftSyntax - -// TODO: [12/22/2024] Remove deprecation warning after ~2 years. -private let warnDeprecatedOnceImpl: Void = { - Issue.ruleDeprecated(ruleID: UnusedCaptureListRule.identifier).print() -}() - -private func warnDeprecatedOnce() { - _ = warnDeprecatedOnceImpl -} - -struct UnusedCaptureListRule: SwiftSyntaxRule, OptInRule { - var configuration = SeverityConfiguration(.warning) - - static let description = RuleDescription( - identifier: "unused_capture_list", - name: "Unused Capture List", - description: "Unused reference in a capture list should be removed", - kind: .lint, - nonTriggeringExamples: [ - Example(""" - [1, 2].map { - [ weak - delegate, - unowned - self - ] num in - delegate.handle(num) - } - """), - Example(""" - [1, 2].map { [weak self] num in - self?.handle(num) - } - """), - Example(""" - let failure: Failure = { [weak self, unowned delegate = self.delegate!] foo in - delegate.handle(foo, self) - } - """), - Example(""" - numbers.forEach({ - [weak handler] in - handler?.handle($0) - }) - """), - Example(""" - withEnvironment(apiService: MockService(fetchProjectResponse: project)) { - [Device.phone4_7inch, Device.phone5_8inch, Device.pad].forEach { device in - device.handle() - } - } - """), - Example("{ [foo] _ in foo.bar() }()"), - Example("sizes.max().flatMap { [(offset: offset, size: $0)] } ?? []"), - Example(""" - [1, 2].map { [self] num in - handle(num) - } - """), - Example(""" - [1, 2].map { [unowned self] num in - handle(num) - } - """), - Example(""" - [1, 2].map { [self, unowned delegate = self.delegate!] num in - delegate.handle(num) - } - """), - Example(""" - [1, 2].map { [unowned self, unowned delegate = self.delegate!] num in - delegate.handle(num) - } - """), - Example(""" - [1, 2].map { - [ weak - delegate, - self - ] num in - delegate.handle(num) - } - """), - Example(""" - rx.onViewDidAppear.subscribe(onNext: { [unowned self] in - doSomething() - }).disposed(by: disposeBag) - """), - Example(""" - let closure = { [weak self] in - guard let self else { - return - } - someInstanceFunction() - } - """), - ], - triggeringExamples: [ - Example(""" - [1, 2].map { [↓weak self] num in - print(num) - } - """), - Example(""" - let failure: Failure = { [weak self, ↓unowned delegate = self.delegate!] foo in - self?.handle(foo) - } - """), - Example(""" - let failure: Failure = { [↓weak self, ↓unowned delegate = self.delegate!] foo in - print(foo) - } - """), - Example(""" - numbers.forEach({ - [weak handler] in - print($0) - }) - """), - Example(""" - numbers.forEach({ - [self, ↓weak handler] in - print($0) - }) - """), - Example(""" - withEnvironment(apiService: MockService(fetchProjectResponse: project)) { [↓foo] in - [Device.phone4_7inch, Device.phone5_8inch, Device.pad].forEach { device in - device.handle() - } - } - """), - Example("{ [↓foo] in _ }()"), - Example(""" - let closure = { [↓weak a] in - // The new `a` immediately shadows the captured `a` which thus isn't needed. - guard let a = getOptionalValue() else { - return - } - someInstanceFunction() - } - """), - ] - ) - - func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { - warnDeprecatedOnce() - return Visitor(configuration: configuration, file: file) - } -} - -private extension UnusedCaptureListRule { - final class Visitor: ViolationsSyntaxVisitor { - override func visitPost(_ node: ClosureExprSyntax) { - guard let captureItems = node.signature?.capture?.items, - captureItems.isNotEmpty else { - return - } - - let captureItemsWithNames = captureItems - .compactMap { item -> (name: String, item: ClosureCaptureSyntax)? in - if let name = item.name { - return (name.text, item) - } - if let expr = item.expression.as(DeclReferenceExprSyntax.self) { - // allow "[unowned self]" - if expr.baseName.tokenKind == .keyword(.self), - item.specifier?.specifier.tokenKind == .keyword(.unowned) { - return nil - } - - // allow "[self]" capture (SE-0269) - if expr.baseName.tokenKind == .keyword(.self), - item.specifier == nil { - return nil - } - - return (expr.baseName.text, item) - } - - return nil - } - - guard captureItemsWithNames.isNotEmpty else { - return - } - - let identifiersToSearch = Set(captureItemsWithNames.map(\.name)) - let foundIdentifiers = IdentifierReferenceVisitor(identifiersToSearch: identifiersToSearch) - .walk(tree: node.statements, handler: \.foundIdentifiers) - - let missingIdentifiers = identifiersToSearch.subtracting(foundIdentifiers) - guard missingIdentifiers.isNotEmpty else { - return - } - - for entry in captureItemsWithNames where missingIdentifiers.contains(entry.name) { - violations.append(entry.item.positionAfterSkippingLeadingTrivia) - } - } - } -} - -private final class IdentifierReferenceVisitor: SyntaxVisitor { - private let identifiersToSearch: Set - private(set) var foundIdentifiers: Set = [] - - init(identifiersToSearch: Set) { - self.identifiersToSearch = identifiersToSearch - super.init(viewMode: .sourceAccurate) - } - - override func visitPost(_ node: DeclReferenceExprSyntax) { - collectReference(by: node.baseName) - } - - override func visitPost(_ node: IdentifierPatternSyntax) { - // Optional bindings without an initializer like `if let self { ... }` reference the captured `self` - // implicitly. This is handled in here. If we have `if let self = self { ... }` the initializer `self` - // is handled in the `IdentifierExprSyntax` case above. - if let binding = node.parent?.as(OptionalBindingConditionSyntax.self), binding.initializer == nil { - collectReference(by: node.identifier) - } - } - - private func collectReference(by token: TokenSyntax) { - let name = token.text - if identifiersToSearch.contains(name) { - foundIdentifiers.insert(name) - } - } -} diff --git a/Tests/GeneratedTests/GeneratedTests.swift b/Tests/GeneratedTests/GeneratedTests.swift index 202bd725f9..14f1c866ad 100644 --- a/Tests/GeneratedTests/GeneratedTests.swift +++ b/Tests/GeneratedTests/GeneratedTests.swift @@ -1333,12 +1333,6 @@ final class UntypedErrorInCatchRuleGeneratedTests: SwiftLintTestCase { } } -final class UnusedCaptureListRuleGeneratedTests: SwiftLintTestCase { - func testWithDefaultConfiguration() { - verifyRule(UnusedCaptureListRule.description) - } -} - final class UnusedClosureParameterRuleGeneratedTests: SwiftLintTestCase { func testWithDefaultConfiguration() { verifyRule(UnusedClosureParameterRule.description) diff --git a/Tests/IntegrationTests/default_rule_configurations.yml b/Tests/IntegrationTests/default_rule_configurations.yml index 7ec4ddeae4..835a0743a4 100644 --- a/Tests/IntegrationTests/default_rule_configurations.yml +++ b/Tests/IntegrationTests/default_rule_configurations.yml @@ -601,8 +601,6 @@ unowned_variable_capture: severity: warning untyped_error_in_catch: severity: warning -unused_capture_list: - severity: warning unused_closure_parameter: severity: warning unused_control_flow_label: