diff --git a/CHANGELOG.md b/CHANGELOG.md index b4f91042ed..2d60164f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,15 @@ #### Enhancements -* None. +* Add `deployment_target` rule to validate that `@availability` attributes and + `#available` conditions are not using a version that is satisfied by the + deployment target. Since SwiftLint can't read an Xcode project, you need to + configure this rule with these keys: `iOS_deployment_target`, + `macOS_deployment_target`, `watchOS_deployment_target` and + `tvOS_deployment_target`. By default, these values are configured with the + minimum versions supported by Swift. + [Marcelo Fabri](https://github.com/marcelofabri) + [#2589](https://github.com/realm/SwiftLint/issues/2589) #### Bug Fixes diff --git a/Rules.md b/Rules.md index 812bc1b4b4..f5c61f7652 100644 --- a/Rules.md +++ b/Rules.md @@ -21,6 +21,7 @@ * [Convenience Type](#convenience-type) * [Custom Rules](#custom-rules) * [Cyclomatic Complexity](#cyclomatic-complexity) +* [Deployment Target](#deployment-target) * [Discarded Notification Center Observer](#discarded-notification-center-observer) * [Discouraged Direct Initialization](#discouraged-direct-initialization) * [Discouraged Object Literal](#discouraged-object-literal) @@ -2900,6 +2901,115 @@ if true {}; if true {}; if true {}; if true {}; if true {} +## Deployment Target + +Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version +--- | --- | --- | --- | --- | --- +`deployment_target` | Enabled | No | lint | No | 4.1.0 + +Availability checks or attributes shouldn't be using older versions that are satisfied by the deployment target. + +### Examples + +<details> +<summary>Non Triggering Examples</summary> + +```swift +@available(iOS 12.0, *) +class A {} +``` + +```swift +@available(watchOS 4.0, *) +class A {} +``` + +```swift +@available(swift 3.0.2) +class A {} +``` + +```swift +class A {} +``` + +```swift +if #available(iOS 10.0, *) {} +``` + +```swift +if #available(iOS 10, *) {} +``` + +```swift +guard #available(iOS 12.0, *) else { return } +``` + +</details> +<details> +<summary>Triggering Examples</summary> + +```swift +↓@available(iOS 6.0, *) +class A {} +``` + +```swift +↓@available(iOS 7.0, *) +class A {} +``` + +```swift +↓@available(iOS 6, *) +class A {} +``` + +```swift +↓@available(iOS 6.0, macOS 10.12, *) + class A {} +``` + +```swift +↓@available(macOS 10.12, iOS 6.0, *) + class A {} +``` + +```swift +↓@available(macOS 10.7, *) +class A {} +``` + +```swift +↓@available(OSX 10.7, *) +class A {} +``` + +```swift +↓@available(watchOS 0.9, *) +class A {} +``` + +```swift +↓@available(tvOS 8, *) +class A {} +``` + +```swift +if ↓#available(iOS 6.0, *) {} +``` + +```swift +if ↓#available(iOS 6, *) {} +``` + +```swift +guard ↓#available(iOS 6.0, *) else { return } +``` + +</details> + + + ## Discarded Notification Center Observer Identifier | Enabled by default | Supports autocorrection | Kind | Analyzer | Minimum Swift Compiler Version diff --git a/Source/SwiftLintFramework/Models/MasterRuleList.swift b/Source/SwiftLintFramework/Models/MasterRuleList.swift index ac01d9112b..ebc9d5ca5d 100644 --- a/Source/SwiftLintFramework/Models/MasterRuleList.swift +++ b/Source/SwiftLintFramework/Models/MasterRuleList.swift @@ -22,6 +22,7 @@ public let masterRuleList = RuleList(rules: [ ConvenienceTypeRule.self, CustomRules.self, CyclomaticComplexityRule.self, + DeploymentTargetRule.self, DiscardedNotificationCenterObserverRule.self, DiscouragedDirectInitRule.self, DiscouragedObjectLiteralRule.self, diff --git a/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift b/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift new file mode 100644 index 0000000000..c6102123d0 --- /dev/null +++ b/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift @@ -0,0 +1,141 @@ +import Foundation +import SourceKittenFramework + +public struct DeploymentTargetRule: ConfigurationProviderRule { + private typealias Version = DeploymentTargetConfiguration.Version + public var configuration = DeploymentTargetConfiguration() + + public init() {} + + public static let description = RuleDescription( + identifier: "deployment_target", + name: "Deployment Target", + description: "Availability checks or attributes shouldn't be using older versions " + + "that are satisfied by the deployment target.", + kind: .lint, + minSwiftVersion: .fourDotOne, + nonTriggeringExamples: [ + "@available(iOS 12.0, *)\nclass A {}", + "@available(watchOS 4.0, *)\nclass A {}", + "@available(swift 3.0.2)\nclass A {}", + "class A {}", + "if #available(iOS 10.0, *) {}", + "if #available(iOS 10, *) {}", + "guard #available(iOS 12.0, *) else { return }" + ], + triggeringExamples: [ + "↓@available(iOS 6.0, *)\nclass A {}", + "↓@available(iOS 7.0, *)\nclass A {}", + "↓@available(iOS 6, *)\nclass A {}", + "↓@available(iOS 6.0, macOS 10.12, *)\n class A {}", + "↓@available(macOS 10.12, iOS 6.0, *)\n class A {}", + "↓@available(macOS 10.7, *)\nclass A {}", + "↓@available(OSX 10.7, *)\nclass A {}", + "↓@available(watchOS 0.9, *)\nclass A {}", + "↓@available(tvOS 8, *)\nclass A {}", + "if ↓#available(iOS 6.0, *) {}", + "if ↓#available(iOS 6, *) {}", + "guard ↓#available(iOS 6.0, *) else { return }" + ] + ) + + public func validate(file: File) -> [StyleViolation] { + var violations = validateAttributes(file: file, dictionary: file.structure.dictionary) + violations += validateConditions(file: file) + violations.sort(by: { $0.location < $1.location }) + + return violations + } + + private func validateConditions(file: File) -> [StyleViolation] { + let pattern = "#available\\s*\\([^\\(]+\\)" + + return file.rangesAndTokens(matching: pattern).flatMap { range, tokens -> [StyleViolation] in + guard let availabilityToken = tokens.first, + SyntaxKind(rawValue: availabilityToken.type) == .keyword, + let tokenRange = file.contents.bridge().byteRangeToNSRange(start: availabilityToken.offset, + length: availabilityToken.length) else { + return [] + } + + let rangeToSearch = NSRange(location: tokenRange.upperBound, length: range.length - tokenRange.length) + return validate(range: rangeToSearch, file: file, violationType: "condition", + byteOffsetToReport: availabilityToken.offset) + } + } + + private func validateAttributes(file: File, dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + return dictionary.substructure.flatMap { subDict -> [StyleViolation] in + var violations = validateAttributes(file: file, dictionary: subDict) + + if let kindString = subDict.kind, + let kind = SwiftDeclarationKind(rawValue: kindString) { + violations += validateAttributes(file: file, kind: kind, dictionary: subDict) + } + + return violations + } + } + + private func validateAttributes(file: File, + kind: SwiftDeclarationKind, + dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] { + let attributes = dictionary.swiftAttributes.filter { + $0.attribute.flatMap(SwiftDeclarationAttributeKind.init) == .available + } + guard !attributes.isEmpty else { + return [] + } + + let contents = file.contents.bridge() + return attributes.flatMap { dictionary -> [StyleViolation] in + guard let offset = dictionary.offset, let length = dictionary.length, + let range = contents.byteRangeToNSRange(start: offset, length: length) else { + return [] + } + + return validate(range: range, file: file, violationType: "attribute", byteOffsetToReport: offset) + }.unique + } + + private func validate(range: NSRange, file: File, violationType: String, + byteOffsetToReport: Int) -> [StyleViolation] { + let platformToConfiguredMinVersion = self.platformToConfiguredMinVersion + let allPlatforms = "(?:" + platformToConfiguredMinVersion.keys.joined(separator: "|") + ")" + let pattern = "\(allPlatforms) [\\d\\.]+" + + return file.rangesAndTokens(matching: pattern, range: range).compactMap { _, tokens -> StyleViolation? in + guard tokens.count == 2, + tokens.kinds == [.keyword, .number], + let platform = file.contents(for: tokens[0]), + let minVersion = platformToConfiguredMinVersion[platform], + let versionString = file.contents(for: tokens[1]) else { + return nil + } + + guard let version = try? Version(rawValue: versionString), + version <= minVersion else { + return nil + } + + let reason = """ + Availability \(violationType) is using a version (\(versionString)) that is \ + satisfied by the deployment target (\(minVersion.stringValue)) for platform \(platform). + """ + return StyleViolation(ruleDescription: type(of: self).description, + severity: configuration.severityConfiguration.severity, + location: Location(file: file, byteOffset: byteOffsetToReport), + reason: reason) + } + } + + private var platformToConfiguredMinVersion: [String: Version] { + return [ + "iOS": configuration.iOSDeploymentTarget, + "macOS": configuration.macOSDeploymentTarget, + "OSX": configuration.macOSDeploymentTarget, + "tvOS": configuration.tvOSDeploymentTarget, + "watchOS": configuration.watchOSDeploymentTarget + ] + } +} diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift new file mode 100644 index 0000000000..7c4e85b00d --- /dev/null +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/DeploymentTargetConfiguration.swift @@ -0,0 +1,106 @@ +public struct DeploymentTargetConfiguration: RuleConfiguration, Equatable { + public struct Version: Equatable, Comparable { + public let major: Int + public let minor: Int + public let patch: Int + + public var stringValue: String { + if patch > 0 { + return "\(major).\(minor).\(patch)" + } else { + return "\(major).\(minor)" + } + } + + public init(major: Int, minor: Int = 0, patch: Int = 0) { + self.major = major + self.minor = minor + self.patch = patch + } + + public init(rawValue: String) throws { + func parseNumber(_ string: String) throws -> Int { + guard let number = Int(string) else { + throw ConfigurationError.unknownConfiguration + } + return number + } + + let parts = rawValue.components(separatedBy: ".") + let count = parts.count + switch count { + case 0: + throw ConfigurationError.unknownConfiguration + case 1: + major = try parseNumber(parts[0]) + minor = 0 + patch = 0 + case 2: + major = try parseNumber(parts[0]) + minor = try parseNumber(parts[1]) + patch = 0 + default: + major = try parseNumber(parts[0]) + minor = try parseNumber(parts[1]) + patch = try parseNumber(parts[2]) + } + } + + fileprivate init(value: Any) throws { + if let version = value as? String { + try self.init(rawValue: version) + } else { + try self.init(rawValue: String(describing: value)) + } + } + + public static func < (lhs: Version, rhs: Version) -> Bool { + if lhs.major != rhs.major { + return lhs.major < rhs.major + } else if lhs.minor != rhs.minor { + return lhs.minor < rhs.minor + } else { + return lhs.patch < rhs.patch + } + } + } + + private(set) var iOSDeploymentTarget = Version(major: 7) + private(set) var macOSDeploymentTarget = Version(major: 10, minor: 9) + private(set) var watchOSDeploymentTarget = Version(major: 1) + private(set) var tvOSDeploymentTarget = Version(major: 9) + + private(set) var severityConfiguration = SeverityConfiguration(.warning) + + public var consoleDescription: String { + return severityConfiguration.consoleDescription + + ", iOS_deployment_target: \(iOSDeploymentTarget.stringValue)" + + ", macOS_deployment_target: \(macOSDeploymentTarget.stringValue)" + + ", watchOS_deployment_target: \(watchOSDeploymentTarget.stringValue)" + + ", tvOS_deployment_target: \(tvOSDeploymentTarget.stringValue)" + } + + public init() {} + + public mutating func apply(configuration: Any) throws { + guard let configuration = configuration as? [String: Any] else { + throw ConfigurationError.unknownConfiguration + } + for (key, value) in configuration { + switch (key, value) { + case ("severity", let severityString as String): + try severityConfiguration.apply(configuration: severityString) + case ("iOS_deployment_target", let deploymentTarget): + self.iOSDeploymentTarget = try Version(value: deploymentTarget) + case ("macOS_deployment_target", let deploymentTarget): + self.macOSDeploymentTarget = try Version(value: deploymentTarget) + case ("watchOS_deployment_target", let deploymentTarget): + self.watchOSDeploymentTarget = try Version(value: deploymentTarget) + case ("tvOS_deployment_target", let deploymentTarget): + self.tvOSDeploymentTarget = try Version(value: deploymentTarget) + default: + throw ConfigurationError.unknownConfiguration + } + } + } +} diff --git a/SwiftLint.xcodeproj/project.pbxproj b/SwiftLint.xcodeproj/project.pbxproj index c7f9bdfb93..2046cc0e05 100644 --- a/SwiftLint.xcodeproj/project.pbxproj +++ b/SwiftLint.xcodeproj/project.pbxproj @@ -228,6 +228,10 @@ D414D6AC21D0B77F00960935 /* DiscouragedObjectLiteralRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D414D6AB21D0B77F00960935 /* DiscouragedObjectLiteralRuleTests.swift */; }; D414D6AE21D22FF500960935 /* LastWhereRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D414D6AD21D22FF500960935 /* LastWhereRule.swift */; }; D41985E721F85014003BE2B7 /* NSLocalizedStringKeyRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41985E621F85014003BE2B7 /* NSLocalizedStringKeyRule.swift */; }; + D41985E921FAB62F003BE2B7 /* DeploymentTargetRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41985E821FAB62F003BE2B7 /* DeploymentTargetRule.swift */; }; + D41985EB21FAB63E003BE2B7 /* DeploymentTargetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41985EA21FAB63E003BE2B7 /* DeploymentTargetConfiguration.swift */; }; + D41985ED21FAD033003BE2B7 /* DeploymentTargetConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41985EC21FAD033003BE2B7 /* DeploymentTargetConfigurationTests.swift */; }; + D41985EF21FAD5E8003BE2B7 /* DeploymentTargetRuleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41985EE21FAD5E8003BE2B7 /* DeploymentTargetRuleTests.swift */; }; D41B57781ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41B57771ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift */; }; D41E7E0B1DF9DABB0065259A /* RedundantStringEnumValueRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41E7E0A1DF9DABB0065259A /* RedundantStringEnumValueRule.swift */; }; D4246D6D1F30D8620097E658 /* PrivateOverFilePrivateRuleConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4246D6C1F30D8620097E658 /* PrivateOverFilePrivateRuleConfiguration.swift */; }; @@ -690,6 +694,10 @@ D414D6AB21D0B77F00960935 /* DiscouragedObjectLiteralRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscouragedObjectLiteralRuleTests.swift; sourceTree = "<group>"; }; D414D6AD21D22FF500960935 /* LastWhereRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWhereRule.swift; sourceTree = "<group>"; }; D41985E621F85014003BE2B7 /* NSLocalizedStringKeyRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSLocalizedStringKeyRule.swift; sourceTree = "<group>"; }; + D41985E821FAB62F003BE2B7 /* DeploymentTargetRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeploymentTargetRule.swift; sourceTree = "<group>"; }; + D41985EA21FAB63E003BE2B7 /* DeploymentTargetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeploymentTargetConfiguration.swift; sourceTree = "<group>"; }; + D41985EC21FAD033003BE2B7 /* DeploymentTargetConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeploymentTargetConfigurationTests.swift; sourceTree = "<group>"; }; + D41985EE21FAD5E8003BE2B7 /* DeploymentTargetRuleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeploymentTargetRuleTests.swift; sourceTree = "<group>"; }; D41B57771ED8CEE0007B0470 /* ExtensionAccessModifierRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtensionAccessModifierRule.swift; sourceTree = "<group>"; }; D41E7E0A1DF9DABB0065259A /* RedundantStringEnumValueRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedundantStringEnumValueRule.swift; sourceTree = "<group>"; }; D4246D6C1F30D8620097E658 /* PrivateOverFilePrivateRuleConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateOverFilePrivateRuleConfiguration.swift; sourceTree = "<group>"; }; @@ -930,6 +938,7 @@ 824AB64C2105C39F004B5A8F /* ConditionalReturnsOnNewlineConfiguration.swift */, 67EB4DF81E4CC101004E9ACD /* CyclomaticComplexityConfiguration.swift */, 62A498551F306A7700D766E4 /* DiscouragedDirectInitConfiguration.swift */, + D41985EA21FAB63E003BE2B7 /* DeploymentTargetConfiguration.swift */, 125AAC77203AA82D0004BCE0 /* ExplicitTypeInterfaceConfiguration.swift */, D4C4A3511DEFBBB700E0E04C /* FileHeaderConfiguration.swift */, 29FFC3781F1574FD007E4825 /* FileLengthRuleConfiguration.swift */, @@ -1024,6 +1033,7 @@ D4B0228D1E0CC608007E5297 /* ClassDelegateProtocolRule.swift */, D4DA1DF31E17511D0037413D /* CompilerProtocolInitRule.swift */, D4DABFD21E29B4A5009617B6 /* DiscardedNotificationCenterObserverRule.swift */, + D41985E821FAB62F003BE2B7 /* DeploymentTargetRule.swift */, 62622F6A1F2F2E3500D5D099 /* DiscouragedDirectInitRule.swift */, E315B83B1DFA4BC500621B44 /* DynamicInlineRule.swift */, 62A3E95B209E078000547A86 /* EmptyXCTestMethodRule.swift */, @@ -1359,6 +1369,8 @@ 67932E2C1E54AF4B00CB0629 /* CyclomaticComplexityConfigurationTests.swift */, 67EB4DFB1E4CD7F5004E9ACD /* CyclomaticComplexityRuleTests.swift */, CCD8B87720559C4A00B75847 /* DisableAllTests.swift */, + D41985EC21FAD033003BE2B7 /* DeploymentTargetConfigurationTests.swift */, + D41985EE21FAD5E8003BE2B7 /* DeploymentTargetRuleTests.swift */, 62AF35D71F30B183009B11EE /* DiscouragedDirectInitRuleTests.swift */, D48B51221F4F5E4B0068AB98 /* DocumentationTests.swift */, D414D6AB21D0B77F00960935 /* DiscouragedObjectLiteralRuleTests.swift */, @@ -1993,6 +2005,7 @@ D4DA1DF41E17511D0037413D /* CompilerProtocolInitRule.swift in Sources */, 629C60D91F43906700B4AF92 /* SingleTestClassRule.swift in Sources */, 621061BF1ED57E640082D51E /* MultilineParametersRuleExamples.swift in Sources */, + D41985EB21FAB63E003BE2B7 /* DeploymentTargetConfiguration.swift in Sources */, D48AE2CC1DFB58C5001C6A4A /* AttributesRuleExamples.swift in Sources */, C28B2B3D2106DF730009A0FE /* PrefixedConstantRuleConfiguration.swift in Sources */, 62A7127520F1178F00E604A6 /* AnyObjectProtocolRule.swift in Sources */, @@ -2066,6 +2079,7 @@ 4A9A3A3A1DC1D75F00DF5183 /* HTMLReporter.swift in Sources */, D40F83881DE9179200524C62 /* TrailingCommaConfiguration.swift in Sources */, 827169B31F488181003FB9AF /* ExplicitEnumRawValueRule.swift in Sources */, + D41985E921FAB62F003BE2B7 /* DeploymentTargetRule.swift in Sources */, 62FE5D32200CABDD00F68793 /* DiscouragedOptionalCollectionExamples.swift in Sources */, D49896F12026B36C00814A83 /* RedundantSetAccessControlRule.swift in Sources */, 29FFC37A1F15764D007E4825 /* FileLengthRuleConfiguration.swift in Sources */, @@ -2116,6 +2130,7 @@ D4F5851520E99A8A0085C6D8 /* TrailingWhitespaceTests.swift in Sources */, D450D1E021F19AB300E60010 /* TrailingClosureRuleTests.swift in Sources */, 3B12C9C31C320A53000B423F /* YamlSwiftLintTests.swift in Sources */, + D41985ED21FAD033003BE2B7 /* DeploymentTargetConfigurationTests.swift in Sources */, D450D1E221F19B1E00E60010 /* TrailingClosureConfigurationTests.swift in Sources */, E832F10D1B17E725003F265F /* IntegrationTests.swift in Sources */, D4C27C001E12DFF500DF713E /* LinterCacheTests.swift in Sources */, @@ -2136,6 +2151,7 @@ D4348EEA1C46122C007707FB /* FunctionBodyLengthRuleTests.swift in Sources */, F480DC831F2609D700099465 /* ConfigurationTests+ProjectMock.swift in Sources */, 3B63D46D1E1F05160057BE35 /* LineLengthConfigurationTests.swift in Sources */, + D41985EF21FAD5E8003BE2B7 /* DeploymentTargetRuleTests.swift in Sources */, 6C7045441C6ADA450003F15A /* SourceKitCrashTests.swift in Sources */, 820F451E21073D7200AA056A /* ConditionalReturnsOnNewlineRuleTests.swift in Sources */, D4246D6F1F30DB260097E658 /* PrivateOverFilePrivateRuleTests.swift in Sources */, diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index ae322bd04c..d445026beb 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -232,6 +232,21 @@ extension CyclomaticComplexityRuleTests { ] } +extension DeploymentTargetConfigurationTests { + static var allTests: [(String, (DeploymentTargetConfigurationTests) -> () throws -> Void)] = [ + ("testAppliesConfigurationFromDictionary", testAppliesConfigurationFromDictionary), + ("testThrowsOnBadConfig", testThrowsOnBadConfig) + ] +} + +extension DeploymentTargetRuleTests { + static var allTests: [(String, (DeploymentTargetRuleTests) -> () throws -> Void)] = [ + ("testRule", testRule), + ("testMacOSAttributeReason", testMacOSAttributeReason), + ("testWatchOSConditionReason", testWatchOSConditionReason) + ] +} + extension DisableAllTests { static var allTests: [(String, (DisableAllTests) -> () throws -> Void)] = [ ("testViolatingPhrase", testViolatingPhrase), @@ -1457,6 +1472,8 @@ XCTMain([ testCase(CustomRulesTests.allTests), testCase(CyclomaticComplexityConfigurationTests.allTests), testCase(CyclomaticComplexityRuleTests.allTests), + testCase(DeploymentTargetConfigurationTests.allTests), + testCase(DeploymentTargetRuleTests.allTests), testCase(DisableAllTests.allTests), testCase(DiscardedNotificationCenterObserverRuleTests.allTests), testCase(DiscouragedDirectInitRuleTests.allTests), diff --git a/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift b/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift new file mode 100644 index 0000000000..9bc76f4ebc --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/DeploymentTargetConfigurationTests.swift @@ -0,0 +1,50 @@ +import SourceKittenFramework +@testable import SwiftLintFramework +import XCTest + +class DeploymentTargetConfigurationTests: XCTestCase { + typealias Version = DeploymentTargetConfiguration.Version + + func testAppliesConfigurationFromDictionary() throws { + var configuration = DeploymentTargetConfiguration() + + try configuration.apply(configuration: ["iOS_deployment_target": "10.1", "severity": "error"]) + XCTAssertEqual(configuration.iOSDeploymentTarget, Version(major: 10, minor: 1)) + XCTAssertEqual(configuration.severityConfiguration.severity, .error) + + try configuration.apply(configuration: ["macOS_deployment_target": "10.11.3"]) + XCTAssertEqual(configuration.iOSDeploymentTarget, Version(major: 10, minor: 1)) + XCTAssertEqual(configuration.macOSDeploymentTarget, Version(major: 10, minor: 11, patch: 3)) + XCTAssertEqual(configuration.severityConfiguration.severity, .error) + + try configuration.apply(configuration: ["severity": "warning"]) + XCTAssertEqual(configuration.iOSDeploymentTarget, Version(major: 10, minor: 1)) + XCTAssertEqual(configuration.macOSDeploymentTarget, Version(major: 10, minor: 11, patch: 3)) + XCTAssertEqual(configuration.severityConfiguration.severity, .warning) + + try configuration.apply(configuration: ["tvOS_deployment_target": 10.2, + "watchOS_deployment_target": 5]) + XCTAssertEqual(configuration.iOSDeploymentTarget, Version(major: 10, minor: 1)) + XCTAssertEqual(configuration.macOSDeploymentTarget, Version(major: 10, minor: 11, patch: 3)) + XCTAssertEqual(configuration.tvOSDeploymentTarget, Version(major: 10, minor: 2)) + XCTAssertEqual(configuration.watchOSDeploymentTarget, Version(major: 5)) + XCTAssertEqual(configuration.severityConfiguration.severity, .warning) + } + + func testThrowsOnBadConfig() { + let badConfigs: [[String: Any]] = [ + ["iOS_deployment_target": "foo"], + ["iOS_deployment_target": ""], + ["iOS_deployment_target": "5.x"], + ["iOS_deployment_target": true], + ["invalid": true] + ] + + for badConfig in badConfigs { + var configuration = DeploymentTargetConfiguration() + checkError(ConfigurationError.unknownConfiguration) { + try configuration.apply(configuration: badConfig) + } + } + } +} diff --git a/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift b/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift new file mode 100644 index 0000000000..3ff0af26ae --- /dev/null +++ b/Tests/SwiftLintFrameworkTests/DeploymentTargetRuleTests.swift @@ -0,0 +1,38 @@ +import SwiftLintFramework +import XCTest + +class DeploymentTargetRuleTests: XCTestCase { + func testRule() { + verifyRule(DeploymentTargetRule.description) + } + + // MARK: - Reasons + + func testMacOSAttributeReason() { + let string = "@availability(macOS 10.11, *)\nclass A {}" + let violations = self.violations(string, config: ["macOS_deployment_target": "10.14.0"]) + + let expectedMessage = "Availability attribute is using a version (10.11) that is satisfied by " + + "the deployment target (10.14) for platform macOS." + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations.first?.reason, expectedMessage) + } + + func testWatchOSConditionReason() { + let string = "if #available(watchOS 4, *) {}" + let violations = self.violations(string, config: ["watchOS_deployment_target": "5.0.1"]) + + let expectedMessage = "Availability condition is using a version (4) that is satisfied by " + + "the deployment target (5.0.1) for platform watchOS." + XCTAssertEqual(violations.count, 1) + XCTAssertEqual(violations.first?.reason, expectedMessage) + } + + private func violations(_ string: String, config: Any?) -> [StyleViolation] { + guard let config = makeConfig(config, DeploymentTargetRule.description.identifier) else { + return [] + } + + return SwiftLintFrameworkTests.violations(string, config: config) + } +}