SwiftLint/Source/SwiftLintFramework/Rules/Lint/DiscardedNotificationCenter...

99 lines
4.7 KiB
Swift

import SwiftSyntax
struct DiscardedNotificationCenterObserverRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule {
var configuration = SeverityConfiguration(.warning)
static let description = RuleDescription(
identifier: "discarded_notification_center_observer",
name: "Discarded Notification Center Observer",
description: "When registering for a notification using a block, the opaque observer that is " +
"returned should be stored so it can be removed later",
kind: .lint,
nonTriggeringExamples: [
Example("let foo = nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }\n"),
Example("""
let foo = nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })
"""),
Example("func foo() -> Any {\n" +
" return nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })\n" +
"}\n"),
Example("var obs: [Any?] = []\n" +
"obs.append(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }))\n"),
Example("""
var obs: [String: Any?] = []
obs["foo"] = nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })
"""),
Example("var obs: [Any?] = []\n" +
"obs.append(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }))\n"),
Example("func foo(_ notif: Any) {\n" +
" obs.append(notif)\n" +
"}\n" +
"foo(nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }))\n"),
Example("""
var obs: [NSObjectProtocol] = [
nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { }),
nc.addObserver(forName: .CKAccountChanged, object: nil, queue: nil, using: { })
]
""")
],
triggeringExamples: [
Example("↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }\n"),
Example("_ = ↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil) { }\n"),
Example("↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })\n"),
Example("""
@discardableResult func foo() -> Any {
return ↓nc.addObserver(forName: .NSSystemTimeZoneDidChange, object: nil, queue: nil, using: { })
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
Visitor(viewMode: .sourceAccurate)
}
}
private extension DiscardedNotificationCenterObserverRule {
final class Visitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: FunctionCallExprSyntax) {
guard
let calledExpression = node.calledExpression.as(MemberAccessExprSyntax.self),
case .identifier("addObserver") = calledExpression.name.tokenKind,
case let argumentLabels = node.argumentList.map({ $0.label?.text }),
argumentLabels.starts(with: ["forName", "object", "queue"])
else {
return
}
if
let firstParent = node.parent?.as(ReturnStmtSyntax.self),
let secondParent = firstParent.parent?.as(CodeBlockItemSyntax.self),
let thirdParent = secondParent.parent?.as(CodeBlockItemListSyntax.self),
let fourthParent = thirdParent.parent?.as(CodeBlockSyntax.self),
let fifthParent = fourthParent.parent?.as(FunctionDeclSyntax.self),
!fifthParent.attributes.hasDiscardableResultAttribute
{
return // result is returned from a function
} else if node.parent?.is(TupleExprElementSyntax.self) == true {
return // result is passed as an argument to a function
} else if node.parent?.is(ArrayElementSyntax.self) == true {
return // result is an array literal element
} else if
let previousToken = node.previousToken,
case .equal = previousToken.tokenKind,
previousToken.previousToken?.tokenKind != .wildcard
{
return // result is assigned to something other than the wildcard keyword (`_`)
}
violations.append(node.positionAfterSkippingLeadingTrivia)
}
}
}
private extension AttributeListSyntax? {
var hasDiscardableResultAttribute: Bool {
contains(attributeNamed: "discardableResult")
}
}