Skip to content

Commit

Permalink
Do not trigger unavailable_condition rule if other #(un)available che…
Browse files Browse the repository at this point in the history
…cks are involved (#4002)
  • Loading branch information
SimplyDanny authored Jun 24, 2022
1 parent a20a75d commit 1faea36
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 11 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
[SimplyDanny](https://github.com/SimplyDanny)
[#3705](https://github.com/realm/SwiftLint/issues/3705)

* Do not trigger `unavailable_condition` rule if other `#(un)available`
checks are involved.
[SimplyDanny](https://github.com/SimplyDanny)
[#3985](https://github.com/realm/SwiftLint/issues/3985)

## 0.47.1: Smarter Appliance

#### Breaking
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ let package = Package(
.target(
name: "SwiftLintFramework",
dependencies: frameworkDependencies,
swiftSettings: swiftSyntaxFiveDotSix ? [.define("SWIFT_SYNTAX_FIVE_DOT_SIX")] : [],
// Pass `-dead_strip_dylibs` to ignore the dynamic version of `lib_InternalSwiftSyntaxParser`
// that ships with SwiftSyntax because we want the static version from
// `StaticInternalSwiftSyntaxParser`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct UnavailableConditionRule: ConfigurationProviderRule, AutomaticTest
public static let description = RuleDescription(
identifier: "unavailable_condition",
name: "Unavailable Condition",
description: "Use #unavailable instead of #available with an empty body.",
description: "Use #unavailable/#available instead of #available/#unavailable with an empty body.",
kind: .idiomatic,
minSwiftVersion: .fiveDotSix,
nonTriggeringExamples: [
Expand All @@ -24,7 +24,23 @@ public struct UnavailableConditionRule: ConfigurationProviderRule, AutomaticTest
} 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("""
Expand All @@ -45,35 +61,83 @@ public struct UnavailableConditionRule: ConfigurationProviderRule, AutomaticTest
if ↓#available(iOS 13, *) {} else {
loadMainWindow()
}
"""),
Example("""
if ↓#unavailable(iOS 13) {
// Do nothing
} else if i < 2 {
loadMainWindow()
}
""")
]
)

public func validate(file: SwiftLintFile) -> [StyleViolation] {
let visitor = UnavailableConditionRuleVisitor()
return visitor.walk(file: file) {
$0.positions
}.map { position in
StyleViolation(ruleDescription: Self.description,
severity: configuration.severity,
location: Location(file: file, byteOffset: ByteCount(position.utf8Offset)))
return visitor.walk(file: file, handler: \.availabilityChecks).map { check in
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severity,
location: Location(
file: file,
byteOffset: ByteCount(check.positionAfterSkippingLeadingTrivia.utf8Offset)),
reason: provideViolationReason(for: check)
)
}
}

private func provideViolationReason(for check: SyntaxProtocol) -> String {
switch check {
case is AvailabilityConditionSyntax:
return "Use #unavailable instead of #available with an empty body."
#if SWIFT_SYNTAX_FIVE_DOT_SIX
case is UnavailabilityConditionSyntax:
return "Use #available instead of #unavailable with an empty body."
#endif
default:
queuedFatalError("Unknown availability check type.")
}
}
}

private final class UnavailableConditionRuleVisitor: SyntaxVisitor {
private(set) var positions: [AbsolutePosition] = []
private(set) var availabilityChecks: [SyntaxProtocol] = []

override func visitPost(_ node: IfStmtSyntax) {
guard node.body.statements.withoutTrivia().isEmpty else {
return
}

guard node.conditions.count == 1, let condition = node.conditions.first,
let availability = condition.condition.as(AvailabilityConditionSyntax.self) else {
let availability = asAvailabilityCondition(condition.condition) else {
return
}

if otherAvailabilityCheckInvolved(ifStmt: node) {
// If there are other conditional branches with availablilty checks it might not be possible
// to just invert the first one.
return
}

positions.append(availability.positionAfterSkippingLeadingTrivia)
availabilityChecks.append(availability)
}

private func asAvailabilityCondition(_ condition: Syntax) -> SyntaxProtocol? {
let availability = condition.as(AvailabilityConditionSyntax.self)
#if SWIFT_SYNTAX_FIVE_DOT_SIX
return availability ?? condition.as(UnavailabilityConditionSyntax.self)
#else
return availability
#endif
}

private func otherAvailabilityCheckInvolved(ifStmt: IfStmtSyntax) -> Bool {
if let elseBody = ifStmt.elseBody, let nestedIfStatement = elseBody.as(IfStmtSyntax.self) {
if nestedIfStatement.conditions.map(\.condition).compactMap(asAvailabilityCondition).isNotEmpty {
return true
}
return otherAvailabilityCheckInvolved(ifStmt: nestedIfStatement)
}
return false
}
}

0 comments on commit 1faea36

Please sign in to comment.