Add new `redundant_self_in_closure` rule (#4911)
This commit is contained in:
parent
e86c06c390
commit
6a2e973de3
|
@ -40,6 +40,7 @@ disabled_rules:
|
||||||
- prefer_nimble
|
- prefer_nimble
|
||||||
- prefer_self_in_static_references
|
- prefer_self_in_static_references
|
||||||
- prefixed_toplevel_constant
|
- prefixed_toplevel_constant
|
||||||
|
- redundant_self_in_closure
|
||||||
- required_deinit
|
- required_deinit
|
||||||
- self_binding
|
- self_binding
|
||||||
- sorted_enum_cases
|
- sorted_enum_cases
|
||||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -18,6 +18,17 @@
|
||||||
* Add `sorted_enum_cases` rule which warns when enum cases are not sorted.
|
* Add `sorted_enum_cases` rule which warns when enum cases are not sorted.
|
||||||
[kimdv](https://github.com/kimdv)
|
[kimdv](https://github.com/kimdv)
|
||||||
|
|
||||||
|
* Add new `redundant_self_in_closure` rule that triggers in closures on
|
||||||
|
explicitly used `self` when it's actually not needed due to:
|
||||||
|
* Strongly captured `self` (`{ [self] in ... }`)
|
||||||
|
* Closure used in a struct declaration (`self` can always be omitted)
|
||||||
|
* Anonymous closures that are directly called (`{ ... }()`) as they are
|
||||||
|
definitly not escaping
|
||||||
|
* Weakly captured `self` with explicit unwrapping
|
||||||
|
|
||||||
|
[SimplyDanny](https://github.com/SimplyDanny)
|
||||||
|
[#59](https://github.com/realm/SwiftLint/issues/59)
|
||||||
|
|
||||||
* Extend `xct_specific_matcher` rule to check for boolean asserts on (un)equal
|
* Extend `xct_specific_matcher` rule to check for boolean asserts on (un)equal
|
||||||
comparisons. The rule can be configured with the matchers that should trigger
|
comparisons. The rule can be configured with the matchers that should trigger
|
||||||
rule violations. By default, all matchers trigger, but that can be limited to
|
rule violations. By default, all matchers trigger, but that can be limited to
|
||||||
|
|
|
@ -165,6 +165,7 @@ public let builtInRules: [Rule.Type] = [
|
||||||
RedundantNilCoalescingRule.self,
|
RedundantNilCoalescingRule.self,
|
||||||
RedundantObjcAttributeRule.self,
|
RedundantObjcAttributeRule.self,
|
||||||
RedundantOptionalInitializationRule.self,
|
RedundantOptionalInitializationRule.self,
|
||||||
|
RedundantSelfInClosureRule.self,
|
||||||
RedundantSetAccessControlRule.self,
|
RedundantSetAccessControlRule.self,
|
||||||
RedundantStringEnumValueRule.self,
|
RedundantStringEnumValueRule.self,
|
||||||
RedundantTypeAnnotationRule.self,
|
RedundantTypeAnnotationRule.self,
|
||||||
|
|
|
@ -0,0 +1,315 @@
|
||||||
|
import SwiftSyntax
|
||||||
|
|
||||||
|
struct RedundantSelfInClosureRule: SwiftSyntaxRule, CorrectableRule, ConfigurationProviderRule, OptInRule {
|
||||||
|
var configuration = SeverityConfiguration(.warning)
|
||||||
|
|
||||||
|
static var description = RuleDescription(
|
||||||
|
identifier: "redundant_self_in_closure",
|
||||||
|
name: "Redundant Self in Closure",
|
||||||
|
description: "Explicit use of 'self' is not required",
|
||||||
|
kind: .style,
|
||||||
|
nonTriggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
struct S {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f {
|
||||||
|
x = 1
|
||||||
|
f { x = 1 }
|
||||||
|
g()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f { [weak self] in
|
||||||
|
self?.x = 1
|
||||||
|
self?.g()
|
||||||
|
guard let self = self ?? C() else { return }
|
||||||
|
self?.x = 1
|
||||||
|
}
|
||||||
|
C().f { self.x = 1 }
|
||||||
|
f { [weak self] in if let self { x = 1 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
],
|
||||||
|
triggeringExamples: [
|
||||||
|
Example("""
|
||||||
|
struct S {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f {
|
||||||
|
↓self.x = 1
|
||||||
|
if ↓self.x == 1 { ↓self.g() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
var x = 0
|
||||||
|
func g() {
|
||||||
|
{
|
||||||
|
↓self.x = 1
|
||||||
|
↓self.g()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f { [self] in
|
||||||
|
↓self.x = 1
|
||||||
|
↓self.g()
|
||||||
|
f { self.x = 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""),
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f { [unowned self] in ↓self.x = 1 }
|
||||||
|
f { [self = self] in ↓self.x = 1 }
|
||||||
|
f { [s = self] in s.x = 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
] + triggeringCompilerSpecificExamples,
|
||||||
|
corrections: [
|
||||||
|
Example("""
|
||||||
|
struct S {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f {
|
||||||
|
↓self.x = 1
|
||||||
|
if ↓self.x == 1 { ↓self.g() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""): Example("""
|
||||||
|
struct S {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f {
|
||||||
|
x = 1
|
||||||
|
if x == 1 { g() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
#if compiler(>=5.8)
|
||||||
|
private static let triggeringCompilerSpecificExamples = [
|
||||||
|
Example("""
|
||||||
|
class C {
|
||||||
|
var x = 0
|
||||||
|
func f(_ work: @escaping () -> Void) { work() }
|
||||||
|
func g() {
|
||||||
|
f { [weak self] in
|
||||||
|
self?.x = 1
|
||||||
|
guard let self else { return }
|
||||||
|
↓self.x = 1
|
||||||
|
}
|
||||||
|
f { [weak self] in
|
||||||
|
self?.x = 1
|
||||||
|
if let self = self else { ↓self.x = 1 }
|
||||||
|
self?.x = 1
|
||||||
|
}
|
||||||
|
f { [weak self] in
|
||||||
|
self?.x = 1
|
||||||
|
while let self else { ↓self.x = 1 }
|
||||||
|
self?.x = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
#else
|
||||||
|
private static let triggeringCompilerSpecificExamples = [Example]()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
|
||||||
|
ScopeVisitor(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func correct(file: SwiftLintFile) -> [Correction] {
|
||||||
|
let ranges = ScopeVisitor(viewMode: .sourceAccurate)
|
||||||
|
.walk(file: file, handler: \.corrections)
|
||||||
|
.compactMap { file.stringView.NSRange(start: $0.start, end: $0.end) }
|
||||||
|
.filter { file.ruleEnabled(violatingRange: $0, for: self) != nil }
|
||||||
|
.reversed()
|
||||||
|
|
||||||
|
var corrections = [Correction]()
|
||||||
|
var contents = file.contents
|
||||||
|
for range in ranges {
|
||||||
|
let contentsNSString = contents.bridge()
|
||||||
|
contents = contentsNSString.replacingCharacters(in: range, with: "")
|
||||||
|
let location = Location(file: file, characterOffset: range.location)
|
||||||
|
corrections.append(Correction(ruleDescription: Self.description, location: location))
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(contents)
|
||||||
|
|
||||||
|
return corrections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TypeDeclarationKind {
|
||||||
|
case likeStruct
|
||||||
|
case likeClass
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum FunctionCallType {
|
||||||
|
case anonymousClosure
|
||||||
|
case function
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SelfCaptureKind {
|
||||||
|
case strong
|
||||||
|
case weak
|
||||||
|
case uncaptured
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScopeVisitor: ViolationsSyntaxVisitor {
|
||||||
|
private var typeDeclarations = Stack<TypeDeclarationKind>()
|
||||||
|
private var functionCalls = Stack<FunctionCallType>()
|
||||||
|
private var selfCaptures = Stack<SelfCaptureKind>()
|
||||||
|
|
||||||
|
private(set) var corrections = [(start: AbsolutePosition, end: AbsolutePosition)]()
|
||||||
|
|
||||||
|
override var skippableDeclarations: [DeclSyntaxProtocol.Type] { .extensionsAndProtocols }
|
||||||
|
|
||||||
|
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
typeDeclarations.push(.likeClass)
|
||||||
|
return .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ActorDeclSyntax) {
|
||||||
|
typeDeclarations.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
typeDeclarations.push(.likeClass)
|
||||||
|
return .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ClassDeclSyntax) {
|
||||||
|
typeDeclarations.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
if let selfItem = node.signature?.capture?.items?.first(where: \.capturesSelf) {
|
||||||
|
selfCaptures.push(selfItem.capturesWeakly ? .weak : .strong)
|
||||||
|
} else {
|
||||||
|
selfCaptures.push(.uncaptured)
|
||||||
|
}
|
||||||
|
return .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: ClosureExprSyntax) {
|
||||||
|
guard let activeTypeDeclarationKind = typeDeclarations.peek(),
|
||||||
|
let activeFunctionCallType = functionCalls.peek(),
|
||||||
|
let activeSelfCaptureKind = selfCaptures.peek() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let localCorrections = ExplicitSelfVisitor(
|
||||||
|
typeDeclarationKind: activeTypeDeclarationKind,
|
||||||
|
functionCallType: activeFunctionCallType,
|
||||||
|
selfCaptureKind: activeSelfCaptureKind
|
||||||
|
).walk(tree: node.statements, handler: \.corrections)
|
||||||
|
violations.append(contentsOf: localCorrections.map(\.start))
|
||||||
|
corrections.append(contentsOf: localCorrections)
|
||||||
|
selfCaptures.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
typeDeclarations.push(.likeStruct)
|
||||||
|
return .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: EnumDeclSyntax) {
|
||||||
|
typeDeclarations.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
if node.calledExpression.is(ClosureExprSyntax.self) {
|
||||||
|
functionCalls.push(.anonymousClosure)
|
||||||
|
} else {
|
||||||
|
functionCalls.push(.function)
|
||||||
|
}
|
||||||
|
return .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: FunctionCallExprSyntax) {
|
||||||
|
functionCalls.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
typeDeclarations.push(.likeStruct)
|
||||||
|
return .visitChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: StructDeclSyntax) {
|
||||||
|
typeDeclarations.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExplicitSelfVisitor: ViolationsSyntaxVisitor {
|
||||||
|
private let typeDeclKind: TypeDeclarationKind
|
||||||
|
private let functionCallType: FunctionCallType
|
||||||
|
private let selfCaptureKind: SelfCaptureKind
|
||||||
|
|
||||||
|
private(set) var corrections = [(start: AbsolutePosition, end: AbsolutePosition)]()
|
||||||
|
|
||||||
|
init(typeDeclarationKind: TypeDeclarationKind,
|
||||||
|
functionCallType: FunctionCallType,
|
||||||
|
selfCaptureKind: SelfCaptureKind) {
|
||||||
|
self.typeDeclKind = typeDeclarationKind
|
||||||
|
self.functionCallType = functionCallType
|
||||||
|
self.selfCaptureKind = selfCaptureKind
|
||||||
|
super.init(viewMode: .sourceAccurate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visitPost(_ node: MemberAccessExprSyntax) {
|
||||||
|
if node.base?.as(IdentifierExprSyntax.self)?.isSelf == true, isSelfRedundant {
|
||||||
|
corrections.append(
|
||||||
|
(start: node.positionAfterSkippingLeadingTrivia, end: node.dot.endPositionBeforeTrailingTrivia)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func visit(_ node: ClosureExprSyntax) -> SyntaxVisitorContinueKind {
|
||||||
|
// Will be handled separately by the parent visitor.
|
||||||
|
.skipChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSelfRedundant: Bool {
|
||||||
|
if typeDeclKind == .likeStruct || functionCallType == .anonymousClosure {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if selfCaptureKind == .strong && SwiftVersion.current >= .fiveDotThree {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if selfCaptureKind == .weak && SwiftVersion.current >= .fiveDotEight {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -328,7 +328,6 @@ public extension IntegerLiteralExprSyntax {
|
||||||
guard case let .integerLiteral(number) = digits.tokenKind else {
|
guard case let .integerLiteral(number) = digits.tokenKind else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return number.isZero
|
return number.isZero
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,11 +337,32 @@ public extension FloatLiteralExprSyntax {
|
||||||
guard case let .floatingLiteral(number) = floatingDigits.tokenKind else {
|
guard case let .floatingLiteral(number) = floatingDigits.tokenKind else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return number.isZero
|
return number.isZero
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public extension IdentifierExprSyntax {
|
||||||
|
var isSelf: Bool {
|
||||||
|
identifier.text == "self"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension MemberAccessExprSyntax {
|
||||||
|
var isSelfAccess: Bool {
|
||||||
|
base?.as(IdentifierExprSyntax.self)?.isSelf == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ClosureCaptureItemSyntax {
|
||||||
|
var capturesSelf: Bool {
|
||||||
|
expression.as(IdentifierExprSyntax.self)?.isSelf == true
|
||||||
|
}
|
||||||
|
|
||||||
|
var capturesWeakly: Bool {
|
||||||
|
specifier?.specifier.text == "weak"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private extension String {
|
private extension String {
|
||||||
var isZero: Bool {
|
var isZero: Bool {
|
||||||
if self == "0" { // fast path
|
if self == "0" { // fast path
|
||||||
|
|
|
@ -33,6 +33,8 @@ public extension SwiftVersion {
|
||||||
static let fiveDotSix = SwiftVersion(rawValue: "5.6.0")
|
static let fiveDotSix = SwiftVersion(rawValue: "5.6.0")
|
||||||
/// Swift 5.7.x - https://swift.org/download/#swift-57
|
/// Swift 5.7.x - https://swift.org/download/#swift-57
|
||||||
static let fiveDotSeven = SwiftVersion(rawValue: "5.7.0")
|
static let fiveDotSeven = SwiftVersion(rawValue: "5.7.0")
|
||||||
|
/// Swift 5.8.x - https://swift.org/download/#swift-58
|
||||||
|
static let fiveDotEight = SwiftVersion(rawValue: "5.8.0")
|
||||||
|
|
||||||
/// The current detected Swift compiler version, based on the currently accessible SourceKit version.
|
/// The current detected Swift compiler version, based on the currently accessible SourceKit version.
|
||||||
///
|
///
|
||||||
|
|
|
@ -980,6 +980,12 @@ class RedundantOptionalInitializationRuleGeneratedTests: SwiftLintTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RedundantSelfInClosureRuleGeneratedTests: SwiftLintTestCase {
|
||||||
|
func testWithDefaultConfiguration() {
|
||||||
|
verifyRule(RedundantSelfInClosureRule.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase {
|
class RedundantSetAccessControlRuleGeneratedTests: SwiftLintTestCase {
|
||||||
func testWithDefaultConfiguration() {
|
func testWithDefaultConfiguration() {
|
||||||
verifyRule(RedundantSetAccessControlRule.description)
|
verifyRule(RedundantSetAccessControlRule.description)
|
||||||
|
|
Loading…
Reference in New Issue