Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Lint/WeakDelegateRule.swift
T
Danny Mösch 58928b7e40 Enforce any on existential types (#5273)
This makes syntactically clear which types are rather expensive.
2023-10-12 08:37:23 +02:00

138 lines
5.1 KiB
Swift

import SwiftSyntax
@SwiftSyntaxRule
struct WeakDelegateRule: OptInRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "weak_delegate",
name: "Weak Delegate",
description: "Delegates should be weak to avoid reference cycles",
kind: .lint,
nonTriggeringExamples: [
Example("class Foo {\n weak var delegate: SomeProtocol?\n}"),
Example("class Foo {\n weak var someDelegate: SomeDelegateProtocol?\n}"),
Example("class Foo {\n weak var delegateScroll: ScrollDelegate?\n}"),
// We only consider properties to be a delegate if it has "delegate" in its name
Example("class Foo {\n var scrollHandler: ScrollDelegate?\n}"),
// Only trigger on instance variables, not local variables
Example("func foo() {\n var delegate: SomeDelegate\n}"),
// Only trigger when variable has the suffix "-delegate" to avoid false positives
Example("class Foo {\n var delegateNotified: Bool?\n}"),
// There's no way to declare a property weak in a protocol
Example("protocol P {\n var delegate: AnyObject? { get set }\n}"),
Example("class Foo {\n protocol P {\n var delegate: AnyObject? { get set }\n}\n}"),
Example("class Foo {\n var computedDelegate: ComputedDelegate {\n return bar() \n} \n}"),
Example("""
class Foo {
var computedDelegate: ComputedDelegate {
get {
return bar()
}
}
"""),
Example("struct Foo {\n @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate \n}"),
Example("struct Foo {\n @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate \n}"),
Example("struct Foo {\n @WKExtensionDelegateAdaptor(ExtensionDelegate.self) var extensionDelegate \n}"),
Example("""
class Foo {
func makeDelegate() -> SomeDelegate {
let delegate = SomeDelegate()
return delegate
}
}
"""),
Example("""
class Foo {
var bar: Bool {
let appDelegate = AppDelegate.bar
return appDelegate.bar
}
}
""", excludeFromDocumentation: true),
Example("private var appDelegate: String?", excludeFromDocumentation: true)
],
triggeringExamples: [
Example("class Foo {\n ↓var delegate: SomeProtocol?\n}"),
Example("class Foo {\n ↓var scrollDelegate: ScrollDelegate?\n}"),
Example("""
class Foo {
↓var delegate: SomeProtocol? {
didSet {
print("Updated delegate")
}
}
""")
]
)
}
private extension WeakDelegateRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }
override func visitPost(_ node: VariableDeclSyntax) {
guard node.hasDelegateSuffix,
node.weakOrUnownedModifier == nil,
!node.hasComputedBody,
!node.containsIgnoredAttribute,
let parent = node.parent,
Syntax(parent).enclosingClass() != nil else {
return
}
violations.append(node.bindingSpecifier.positionAfterSkippingLeadingTrivia)
}
}
}
private extension Syntax {
func enclosingClass() -> ClassDeclSyntax? {
if let classExpr = self.as(ClassDeclSyntax.self) {
return classExpr
} else if self.as(DeclSyntax.self) != nil {
return nil
}
return parent?.enclosingClass()
}
}
private extension VariableDeclSyntax {
var hasDelegateSuffix: Bool {
bindings.allSatisfy { binding in
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
return false
}
return pattern.identifier.text.lowercased().hasSuffix("delegate")
}
}
var hasComputedBody: Bool {
bindings.allSatisfy { binding in
if case .getter = binding.accessorBlock?.accessors {
return true
}
return binding.accessorBlock?.specifiesGetAccessor == true
}
}
var containsIgnoredAttribute: Bool {
let ignoredAttributes: Set = [
"UIApplicationDelegateAdaptor",
"NSApplicationDelegateAdaptor",
"WKExtensionDelegateAdaptor"
]
return attributes.contains { attr in
guard case let .attribute(customAttr) = attr,
let typeIdentifier = customAttr.attributeName.as(IdentifierTypeSyntax.self) else {
return false
}
return ignoredAttributes.contains(typeIdentifier.name.text)
}
}
}