mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
Extend redundant_self_in_closure to find all redundant selfs (#6346)
This commit is contained in:
@@ -41,6 +41,7 @@ disabled_rules:
|
||||
- one_declaration_per_file
|
||||
- prefer_nimble
|
||||
- prefixed_toplevel_constant
|
||||
- redundant_self_in_closure
|
||||
- required_deinit
|
||||
- sorted_enum_cases
|
||||
- strict_fileprivate
|
||||
|
||||
+8
-1
@@ -4,7 +4,9 @@
|
||||
|
||||
### Breaking
|
||||
|
||||
* None.
|
||||
* The `redundant_self_in_closure` rule has been renamed to `redundant_self` (with
|
||||
`redundant_self_in_closure` as a deprecated alias) to reflect its now broader scope.
|
||||
[SimplyDanny](https://github.com/SimplyDanny)
|
||||
|
||||
### Experimental
|
||||
|
||||
@@ -12,6 +14,11 @@
|
||||
|
||||
### Enhancements
|
||||
|
||||
* Extend `redundant_self_in_closure` rule to detect all redundant uses of `self`,
|
||||
not just in closures. Initializers (which commonly prefer an explicit `self` prefix)
|
||||
can be ignored by setting `keep_in_initializers` to `true`.
|
||||
[SimplyDanny](https://github.com/SimplyDanny)
|
||||
|
||||
* Add a `separation` configuration option to the `vertical_whitespace_between_cases` rule
|
||||
to allow customizing blank line separation between switch cases. The default value is
|
||||
`always` (require at least one blank line). Setting it to `never` enforces no blank
|
||||
|
||||
@@ -178,7 +178,7 @@ public let builtInRules: [any Rule.Type] = [
|
||||
RedundantDiscardableLetRule.self,
|
||||
RedundantNilCoalescingRule.self,
|
||||
RedundantObjcAttributeRule.self,
|
||||
RedundantSelfInClosureRule.self,
|
||||
RedundantSelfRule.self,
|
||||
RedundantSendableRule.self,
|
||||
RedundantSetAccessControlRule.self,
|
||||
RedundantStringEnumValueRule.self,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import SwiftLintCore
|
||||
|
||||
@AutoConfigParser
|
||||
struct RedundantSelfConfiguration: SeverityBasedRuleConfiguration {
|
||||
@ConfigurationElement(key: "severity")
|
||||
private(set) var severityConfiguration = SeverityConfiguration<Parent>(.warning)
|
||||
@ConfigurationElement(key: "keep_in_initializers")
|
||||
private(set) var keepInInitializers = false
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import SwiftSyntax
|
||||
|
||||
@SwiftSyntaxRule(correctable: true, optIn: true)
|
||||
struct RedundantSelfInClosureRule: Rule {
|
||||
var configuration = SeverityConfiguration<Self>(.warning)
|
||||
|
||||
static let description = RuleDescription(
|
||||
identifier: "redundant_self_in_closure",
|
||||
name: "Redundant Self in Closure",
|
||||
description: "Explicit use of 'self' is not required",
|
||||
kind: .style,
|
||||
nonTriggeringExamples: RedundantSelfInClosureRuleExamples.nonTriggeringExamples,
|
||||
triggeringExamples: RedundantSelfInClosureRuleExamples.triggeringExamples,
|
||||
corrections: RedundantSelfInClosureRuleExamples.corrections
|
||||
)
|
||||
}
|
||||
|
||||
private enum TypeDeclarationKind {
|
||||
case likeStruct
|
||||
case likeClass
|
||||
}
|
||||
|
||||
private enum FunctionCallType {
|
||||
case anonymousClosure
|
||||
case function
|
||||
}
|
||||
|
||||
private enum SelfCaptureKind {
|
||||
case strong
|
||||
case weak
|
||||
case uncaptured
|
||||
}
|
||||
|
||||
private extension RedundantSelfInClosureRule {
|
||||
final class Visitor: DeclaredIdentifiersTrackingVisitor<ConfigurationType> {
|
||||
private var typeDeclarations = Stack<TypeDeclarationKind>()
|
||||
private var functionCalls = Stack<FunctionCallType>()
|
||||
private var selfCaptures = Stack<SelfCaptureKind>()
|
||||
|
||||
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
|
||||
|
||||
override func visit(_: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeClass)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: ActorDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
override func visit(_: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeClass)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: ClassDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||
if let selfItem = node.signature?.capture?.items.first(where: \.capturesSelf) {
|
||||
selfCaptures.push(selfItem.capturesWeakly ? .weak : .strong)
|
||||
} else {
|
||||
selfCaptures.push(.uncaptured)
|
||||
}
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_ node: ClosureExprSyntax) {
|
||||
guard let activeTypeDeclarationKind = typeDeclarations.peek(),
|
||||
let activeFunctionCallType = functionCalls.peek(),
|
||||
let activeSelfCaptureKind = selfCaptures.peek() else {
|
||||
return
|
||||
}
|
||||
let localViolationCorrections = ExplicitSelfVisitor(
|
||||
configuration: configuration,
|
||||
file: file,
|
||||
typeDeclarationKind: activeTypeDeclarationKind,
|
||||
functionCallType: activeFunctionCallType,
|
||||
selfCaptureKind: activeSelfCaptureKind,
|
||||
scope: scope
|
||||
).walk(tree: node.statements, handler: \.violations)
|
||||
violations.append(contentsOf: localViolationCorrections)
|
||||
selfCaptures.pop()
|
||||
}
|
||||
|
||||
override func visit(_: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeStruct)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: EnumDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
|
||||
if node.calledExpression.is(ClosureExprSyntax.self) {
|
||||
functionCalls.push(.anonymousClosure)
|
||||
} else {
|
||||
functionCalls.push(.function)
|
||||
}
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: FunctionCallExprSyntax) {
|
||||
functionCalls.pop()
|
||||
}
|
||||
|
||||
override func visit(_: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeStruct)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: StructDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ExplicitSelfVisitor<Configuration: RuleConfiguration>: DeclaredIdentifiersTrackingVisitor<Configuration> {
|
||||
private let typeDeclKind: TypeDeclarationKind
|
||||
private let functionCallType: FunctionCallType
|
||||
private let selfCaptureKind: SelfCaptureKind
|
||||
|
||||
init(configuration: Configuration,
|
||||
file: SwiftLintFile,
|
||||
typeDeclarationKind: TypeDeclarationKind,
|
||||
functionCallType: FunctionCallType,
|
||||
selfCaptureKind: SelfCaptureKind,
|
||||
scope: Scope) {
|
||||
self.typeDeclKind = typeDeclarationKind
|
||||
self.functionCallType = functionCallType
|
||||
self.selfCaptureKind = selfCaptureKind
|
||||
super.init(configuration: configuration, file: file, scope: scope)
|
||||
}
|
||||
|
||||
override func visitPost(_ node: MemberAccessExprSyntax) {
|
||||
if !hasSeenDeclaration(for: node.declName.baseName.text), node.isBaseSelf, isSelfRedundant {
|
||||
violations.append(
|
||||
at: node.positionAfterSkippingLeadingTrivia,
|
||||
correction: .init(
|
||||
start: node.positionAfterSkippingLeadingTrivia,
|
||||
end: node.period.endPositionBeforeTrailingTrivia,
|
||||
replacement: ""
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override func visit(_: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||
// Will be handled separately by the parent visitor.
|
||||
.skipChildren
|
||||
}
|
||||
|
||||
var isSelfRedundant: Bool {
|
||||
typeDeclKind == .likeStruct
|
||||
|| functionCallType == .anonymousClosure
|
||||
|| selfCaptureKind == .strong && SwiftVersion.current >= .fiveDotThree
|
||||
|| selfCaptureKind == .weak && SwiftVersion.current >= .fiveDotEight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
@_spi(Diagnostics)
|
||||
import SwiftParser
|
||||
@_spi(RawSyntax)
|
||||
import SwiftSyntax
|
||||
|
||||
@SwiftSyntaxRule(correctable: true, optIn: true)
|
||||
struct RedundantSelfRule: Rule {
|
||||
var configuration = RedundantSelfConfiguration()
|
||||
|
||||
static let description = RuleDescription(
|
||||
identifier: "redundant_self",
|
||||
name: "Redundant Self",
|
||||
description: "Explicit use of 'self' is not required",
|
||||
kind: .style,
|
||||
nonTriggeringExamples: RedundantSelfRuleExamples.nonTriggeringExamples,
|
||||
triggeringExamples: RedundantSelfRuleExamples.triggeringExamples,
|
||||
corrections: RedundantSelfRuleExamples.corrections,
|
||||
deprecatedAliases: ["redundant_self_in_closure"]
|
||||
)
|
||||
}
|
||||
|
||||
private enum TypeDeclarationKind {
|
||||
case likeStruct, likeClass
|
||||
}
|
||||
|
||||
private enum ClosureExprType {
|
||||
case anonymousCall, functionArgument
|
||||
}
|
||||
|
||||
private enum SelfCaptureKind {
|
||||
case strong, weak, uncaptured
|
||||
}
|
||||
|
||||
private extension RedundantSelfRule {
|
||||
final class Visitor: DeclaredIdentifiersTrackingVisitor<ConfigurationType> {
|
||||
private var typeDeclarations = Stack<TypeDeclarationKind>()
|
||||
private var closureExprScopes = Stack<(ClosureExprType, SelfCaptureKind)>()
|
||||
private var initializerScopes = Stack<Bool>()
|
||||
|
||||
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
|
||||
|
||||
override func visit(_: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeClass)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: ActorDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
override func visit(_: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeClass)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: ClassDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||
let captureType: SelfCaptureKind =
|
||||
if let selfItem = node.signature?.capture?.items.first(where: \.capturesSelf) {
|
||||
selfItem.capturesWeakly ? .weak : .strong
|
||||
} else {
|
||||
.uncaptured
|
||||
}
|
||||
let exprType: ClosureExprType =
|
||||
if node.keyPathInParent == \FunctionCallExprSyntax.calledExpression {
|
||||
.anonymousCall
|
||||
} else {
|
||||
.functionArgument
|
||||
}
|
||||
closureExprScopes.push((exprType, captureType))
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: ClosureExprSyntax) {
|
||||
closureExprScopes.pop()
|
||||
}
|
||||
|
||||
override func visit(_: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeStruct)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: EnumDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
override func visit(_: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
initializerScopes.push(true)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: InitializerDeclSyntax) {
|
||||
initializerScopes.pop()
|
||||
}
|
||||
|
||||
override func visitPost(_ node: MemberAccessExprSyntax) {
|
||||
if configuration.keepInInitializers, initializerScopes.peek() == true {
|
||||
return
|
||||
}
|
||||
if closureExprScopes.isNotEmpty, !isSelfRedundant {
|
||||
return
|
||||
}
|
||||
let declName = node.declName.baseName.text
|
||||
if !hasSeenDeclaration(for: declName), node.isBaseSelf, declName != "init" {
|
||||
violations.append(
|
||||
at: node.positionAfterSkippingLeadingTrivia,
|
||||
correction: .init(
|
||||
start: node.positionAfterSkippingLeadingTrivia,
|
||||
end: node.endPositionBeforeTrailingTrivia,
|
||||
replacement: node.declName.baseName.needsEscaping
|
||||
? "`\(declName)`"
|
||||
: declName
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override func visit(_: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
typeDeclarations.push(.likeStruct)
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
override func visitPost(_: StructDeclSyntax) {
|
||||
typeDeclarations.pop()
|
||||
}
|
||||
|
||||
private var isSelfRedundant: Bool {
|
||||
if typeDeclarations.peek() == .likeStruct {
|
||||
return true
|
||||
}
|
||||
guard let (closureType, selfCapture) = closureExprScopes.peek() else {
|
||||
return false
|
||||
}
|
||||
return closureType == .anonymousCall
|
||||
|| selfCapture == .strong && SwiftVersion.current >= .fiveDotThree
|
||||
|| selfCapture == .weak && SwiftVersion.current >= .fiveDotEight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension TokenSyntax {
|
||||
var needsEscaping: Bool {
|
||||
[UInt8](text.utf8).withUnsafeBufferPointer {
|
||||
if let keyword = Keyword(SyntaxText(baseAddress: $0.baseAddress, count: text.count)) {
|
||||
return TokenKind.keyword(keyword).isLexerClassifiedKeyword
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-3
@@ -1,4 +1,4 @@
|
||||
struct RedundantSelfInClosureRuleExamples {
|
||||
struct RedundantSelfRuleExamples {
|
||||
static let nonTriggeringExamples = [
|
||||
Example("""
|
||||
struct S {
|
||||
@@ -93,6 +93,15 @@ struct RedundantSelfInClosureRuleExamples {
|
||||
func f(_: () -> Void) {}
|
||||
}
|
||||
""", excludeFromDocumentation: true),
|
||||
Example("""
|
||||
class C {
|
||||
var x = 0, y = 0
|
||||
init(x: Int) {
|
||||
self.x = x
|
||||
self.y = x + 1
|
||||
}
|
||||
}
|
||||
""", configuration: ["keep_in_initializers": true]),
|
||||
]
|
||||
|
||||
static let triggeringExamples = [
|
||||
@@ -194,11 +203,11 @@ struct RedundantSelfInClosureRuleExamples {
|
||||
var x = 0
|
||||
func f(_ work: @escaping () -> Void) { work() }
|
||||
func g() {
|
||||
f { [weak self] in
|
||||
f({ [weak self] in
|
||||
self?.x = 1
|
||||
guard let self else { return }
|
||||
↓self.x = 1
|
||||
}
|
||||
})
|
||||
f { [weak self] in
|
||||
self?.x = 1
|
||||
if let self = self { ↓self.x = 1 }
|
||||
@@ -212,6 +221,23 @@ struct RedundantSelfInClosureRuleExamples {
|
||||
}
|
||||
}
|
||||
"""),
|
||||
Example("""
|
||||
class C {
|
||||
var x = 0
|
||||
private lazy var c1: Int = {
|
||||
↓self.x = 1
|
||||
let f = { self.x = 2 }
|
||||
let g = { [self] in ↓self.x = 3 }
|
||||
return 2
|
||||
}()
|
||||
private lazy var c2: Int = { [weak self] in
|
||||
guard let self else { return 0 }
|
||||
↓self.x = 1
|
||||
let f = { self.x = 2 }
|
||||
return 2
|
||||
}()
|
||||
}
|
||||
""", excludeFromDocumentation: true),
|
||||
]
|
||||
|
||||
static let corrections = [
|
||||
@@ -238,5 +264,22 @@ struct RedundantSelfInClosureRuleExamples {
|
||||
}
|
||||
}
|
||||
"""),
|
||||
Example("""
|
||||
struct S {
|
||||
var x = 0, y = 0
|
||||
init(x: Int) {
|
||||
self.x = x
|
||||
↓self.y = 1
|
||||
}
|
||||
}
|
||||
"""): Example("""
|
||||
struct S {
|
||||
var x = 0, y = 0
|
||||
init(x: Int) {
|
||||
self.x = x
|
||||
y = 1
|
||||
}
|
||||
}
|
||||
"""),
|
||||
]
|
||||
}
|
||||
@@ -49,9 +49,21 @@ public struct Stack<Element> {
|
||||
}
|
||||
}
|
||||
|
||||
extension Stack: Sequence {
|
||||
public func makeIterator() -> [Element].Iterator {
|
||||
elements.makeIterator()
|
||||
extension Stack: Collection {
|
||||
public var startIndex: Int {
|
||||
elements.startIndex
|
||||
}
|
||||
|
||||
public var endIndex: Int {
|
||||
elements.endIndex
|
||||
}
|
||||
|
||||
public subscript(position: Int) -> Element {
|
||||
elements[position]
|
||||
}
|
||||
|
||||
public func index(after index: Int) -> Int {
|
||||
elements.index(after: index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,13 @@ open class DeclaredIdentifiersTrackingVisitor<Configuration: RuleConfiguration>:
|
||||
}
|
||||
}
|
||||
|
||||
override open func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||
if node.parent?.is(MemberBlockItemSyntax.self) != true {
|
||||
scope.addToCurrentScope(.localVariable(name: node.name))
|
||||
}
|
||||
return .visitChildren
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
|
||||
private func collectIdentifiers(from parameters: FunctionParameterListSyntax) {
|
||||
|
||||
@@ -13,9 +13,9 @@ final class RedundantObjcAttributeRuleGeneratedTests: SwiftLintTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
final class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase {
|
||||
final class RedundantSelfRuleGeneratedTests: SwiftLintTestCase {
|
||||
func testWithDefaultConfiguration() {
|
||||
verifyRule(RedundantSelfInClosureRule.description)
|
||||
verifyRule(RedundantSelfRule.description)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1014,8 +1014,9 @@ redundant_objc_attribute:
|
||||
meta:
|
||||
opt-in: false
|
||||
correctable: true
|
||||
redundant_self_in_closure:
|
||||
redundant_self:
|
||||
severity: warning
|
||||
keep_in_initializers: false
|
||||
meta:
|
||||
opt-in: true
|
||||
correctable: true
|
||||
|
||||
Reference in New Issue
Block a user