From 1752587b7b255ab858cb5ffb457df6a656847731 Mon Sep 17 00:00:00 2001 From: Marcelo Fabri Date: Sun, 17 Apr 2022 03:30:53 -0700 Subject: [PATCH] Add unavailable_condition rule (#3953) Fixes #3897 --- CHANGELOG.md | 6 ++ .../Models/PrimaryRuleList.swift | 1 + .../Idiomatic/UnavailableConditionRule.swift | 82 +++++++++++++++++++ .../AutomaticRuleTests.generated.swift | 6 ++ 4 files changed, 95 insertions(+) create mode 100644 Source/SwiftLintFramework/Rules/Idiomatic/UnavailableConditionRule.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 84173b9a67..edc4cf78e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ [Marcelo Fabri](https://github.com/marcelofabri) [#3343](https://github.com/realm/SwiftLint/issues/3343) +* Add `unavailable_condition` rule to prefer using `if #unavailable` instead of + `if #available` with an empty body and an `else` condition when using + Swift 5.6 or later. + [Marcelo Fabri](https://github.com/marcelofabri) + [#3897](https://github.com/realm/SwiftLint/issues/3897) + #### Bug Fixes * Fix false positives in `unused_closure_parameter` when using parameters with diff --git a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift index 08b29a876c..da0000d260 100644 --- a/Source/SwiftLintFramework/Models/PrimaryRuleList.swift +++ b/Source/SwiftLintFramework/Models/PrimaryRuleList.swift @@ -187,6 +187,7 @@ public let primaryRuleList = RuleList(rules: [ TypeContentsOrderRule.self, TypeNameRule.self, TypesafeArrayInitRule.self, + UnavailableConditionRule.self, UnavailableFunctionRule.self, UnneededBreakInSwitchRule.self, UnneededParenthesesInClosureArgumentRule.self, diff --git a/Source/SwiftLintFramework/Rules/Idiomatic/UnavailableConditionRule.swift b/Source/SwiftLintFramework/Rules/Idiomatic/UnavailableConditionRule.swift new file mode 100644 index 0000000000..c296b78a2e --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Idiomatic/UnavailableConditionRule.swift @@ -0,0 +1,82 @@ +import SourceKittenFramework +import SwiftSyntax + +public struct UnavailableConditionRule: ConfigurationProviderRule, AutomaticTestableRule { + public var configuration = SeverityConfiguration(.warning) + + public init() {} + + public static let description = RuleDescription( + identifier: "unavailable_condition", + name: "Unavailable Condition", + description: "Use #unavailable instead of #available with an empty body.", + kind: .idiomatic, + minSwiftVersion: .fiveDotSix, + nonTriggeringExamples: [ + Example(""" + if #unavailable(iOS 13) { + loadMainWindow() + } + """), + Example(""" + if #available(iOS 9.0, *) { + doSomething() + } else { + legacyDoSomething() + } + """) + ], + 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() + } + """) + ] + ) + + public func validate(file: SwiftLintFile) -> [StyleViolation] { + guard let tree = file.syntaxTree else { + return [] + } + + let visitor = UnavailableConditionRuleVisitor() + visitor.walk(tree) + return visitor.positions.map { position in + StyleViolation(ruleDescription: Self.description, + severity: configuration.severity, + location: Location(file: file, byteOffset: ByteCount(position.utf8Offset))) + } + } +} + +private final class UnavailableConditionRuleVisitor: SyntaxVisitor { + private(set) var positions: [AbsolutePosition] = [] + + 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 { + return + } + + positions.append(availability.positionAfterSkippingLeadingTrivia) + } +} diff --git a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift index 9dbbf964c2..1a1a5934fe 100644 --- a/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift +++ b/Tests/SwiftLintFrameworkTests/AutomaticRuleTests.generated.swift @@ -797,6 +797,12 @@ class TypesafeArrayInitRuleTests: XCTestCase { } } +class UnavailableConditionRuleTests: XCTestCase { + func testWithDefaultConfiguration() { + verifyRule(UnavailableConditionRule.description) + } +} + class UnavailableFunctionRuleTests: XCTestCase { func testWithDefaultConfiguration() { verifyRule(UnavailableFunctionRule.description)