SwiftLint/Source/SwiftLintFramework/Rules/Idiomatic/UnavailableConditionRule.swift

128 lines
4.2 KiB
Swift

import SwiftSyntax
struct UnavailableConditionRule: ConfigurationProviderRule, SwiftSyntaxRule {
var configuration = SeverityConfiguration(.warning)
static let description = RuleDescription(
identifier: "unavailable_condition",
name: "Unavailable Condition",
description: "Use #unavailable/#available instead of #available/#unavailable with an empty body.",
kind: .idiomatic,
minSwiftVersion: .fiveDotSix,
nonTriggeringExamples: [
Example("""
if #unavailable(iOS 13) {
loadMainWindow()
}
"""),
Example("""
if #available(iOS 9.0, *) {
doSomething()
} else {
legacyDoSomething()
}
"""),
Example("""
if #available(macOS 11.0, *) {
// Do nothing
} else if #available(macOS 10.15, *) {
print("do some stuff")
}
"""),
Example("""
if #available(macOS 11.0, *) {
// Do nothing
} else if i > 7 {
print("do some stuff")
} else if i < 2, #available(macOS 11.0, *) {
print("something else")
}
""", excludeFromDocumentation: true)
],
triggeringExamples: [
Example("""
if ↓#available(iOS 14.0) {
} else {
oldIos13TrackingLogic(isEnabled: ASIdentifierManager.shared().isAdvertisingTrackingEnabled)
}
"""),
Example("""
if ↓#available(iOS 14.0) {
// we don't need to do anything here
} else {
oldIos13TrackingLogic(isEnabled: ASIdentifierManager.shared().isAdvertisingTrackingEnabled)
}
"""),
Example("""
if ↓#available(iOS 13, *) {} else {
loadMainWindow()
}
"""),
Example("""
if ↓#unavailable(iOS 13) {
// Do nothing
} else if i < 2 {
loadMainWindow()
}
""")
]
)
func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
UnavailableConditionRuleVisitor(viewMode: .sourceAccurate)
}
}
private final class UnavailableConditionRuleVisitor: ViolationsSyntaxVisitor {
override func visitPost(_ node: IfExprSyntax) {
guard node.body.statements.isEmpty else {
return
}
guard let condition = node.conditions.onlyElement,
let availability = asAvailabilityCondition(condition.condition) else {
return
}
if otherAvailabilityCheckInvolved(ifStmt: node) {
// If there are other conditional branches with availability checks it might not be possible
// to just invert the first one.
return
}
violations.append(
ReasonedRuleViolation(
position: availability.positionAfterSkippingLeadingTrivia,
reason: reason(for: availability)
)
)
}
private func asAvailabilityCondition(_ condition: ConditionElementSyntax.Condition)
-> AvailabilityConditionSyntax? {
condition.as(AvailabilityConditionSyntax.self)
}
private func otherAvailabilityCheckInvolved(ifStmt: IfExprSyntax) -> Bool {
if let elseBody = ifStmt.elseBody, let nestedIfStatement = elseBody.as(IfExprSyntax.self) {
if nestedIfStatement.conditions.map(\.condition).compactMap(asAvailabilityCondition).isNotEmpty {
return true
}
return otherAvailabilityCheckInvolved(ifStmt: nestedIfStatement)
}
return false
}
private func reason(for condition: AvailabilityConditionSyntax) -> String {
switch condition.availabilityKeyword.tokenKind {
case .poundAvailableKeyword:
return "Use #unavailable instead of #available with an empty body"
case .poundUnavailableKeyword:
return "Use #available instead of #unavailable with an empty body"
default:
queuedFatalError("Unknown availability check type.")
}
}
}