diff --git a/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift b/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift index 25d8a6aa66..1047c8439a 100644 --- a/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift +++ b/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRule.swift @@ -1,7 +1,6 @@ -import Foundation -import SourceKittenFramework +import SwiftSyntax -public struct DeploymentTargetRule: ConfigurationProviderRule { +public struct DeploymentTargetRule: ConfigurationProviderRule, SourceKitFreeRule { private typealias Version = DeploymentTargetConfiguration.Version public var configuration = DeploymentTargetConfiguration() @@ -18,95 +17,17 @@ public struct DeploymentTargetRule: ConfigurationProviderRule { ) public func validate(file: SwiftLintFile) -> [StyleViolation] { - var violations = validateAttributes(file: file, dictionary: file.structureDictionary) - violations += validateConditions(file: file, type: .condition) - violations += validateConditions(file: file, type: .negativeCondition) - violations.sort(by: { $0.location < $1.location }) - - return violations - } - - private func validateConditions(file: SwiftLintFile, type: AvailabilityType) -> [StyleViolation] { - guard SwiftVersion.current >= type.requiredSwiftVersion else { - return [] - } - - let pattern = "#\(type.keyword)\\s*\\([^\\(]+\\)" - - return file.rangesAndTokens(matching: pattern).flatMap { range, tokens -> [StyleViolation] in - guard let availabilityToken = tokens.first, - availabilityToken.kind == .keyword, - let tokenRange = file.stringView.byteRangeToNSRange(availabilityToken.range) - else { - return [] - } - - let rangeToSearch = NSRange(location: tokenRange.upperBound, length: range.length - tokenRange.length) - return validate(range: rangeToSearch, file: file, violationType: type, - byteOffsetToReport: availabilityToken.offset) - } - } - - private func validateAttributes(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [StyleViolation] { - return dictionary.traverseDepthFirst { subDict in - guard let kind = subDict.declarationKind else { return nil } - return validateAttributes(file: file, kind: kind, dictionary: subDict) - } - } - - private func validateAttributes(file: SwiftLintFile, - kind: SwiftDeclarationKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - let attributes = dictionary.swiftAttributes.filter { - $0.attribute.flatMap(SwiftDeclarationAttributeKind.init) == .available - } - guard attributes.isNotEmpty else { - return [] - } - - let contents = file.stringView - return attributes.flatMap { dictionary -> [StyleViolation] in - guard let byteRange = dictionary.byteRange, - let range = contents.byteRangeToNSRange(byteRange) - else { - return [] - } - - return validate(range: range, file: file, violationType: .attribute, - byteOffsetToReport: byteRange.location) - }.unique - } - - private func validate(range: NSRange, file: SwiftLintFile, violationType: AvailabilityType, - byteOffsetToReport: ByteCount) -> [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 platformString = file.contents(for: tokens[0]), - let platform = DeploymentTargetConfiguration.Platform(rawValue: platformString), - let minVersion = platformToConfiguredMinVersion[platformString], - let versionString = file.contents(for: tokens[1]) else { - return nil + return Visitor(platformToConfiguredMinVersion: platformToConfiguredMinVersion) + .walk(file: file, handler: \.violationPositions) + .sorted(by: { $0.position < $1.position }) + .map { position, reason in + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severityConfiguration.severity, + location: Location(file: file, position: position), + reason: reason + ) } - - guard let version = try? Version(platform: platform, rawValue: versionString), - version <= minVersion else { - return nil - } - - let reason = """ - Availability \(violationType.displayString) is using a version (\(versionString)) that is \ - satisfied by the deployment target (\(minVersion.stringValue)) for platform \(platformString). - """ - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: Location(file: file, byteOffset: byteOffsetToReport), - reason: reason) - } } private var platformToConfiguredMinVersion: [String: Version] { @@ -138,23 +59,79 @@ public struct DeploymentTargetRule: ConfigurationProviderRule { return "negative condition" } } + } +} - var keyword: String { - switch self { - case .condition, .attribute: - return "available" - case .negativeCondition: - return "unavailable" +private extension DeploymentTargetRule { + private final class Visitor: SyntaxVisitor { + private(set) var violationPositions: [(position: AbsolutePosition, reason: String)] = [] + private let platformToConfiguredMinVersion: [String: Version] + + init(platformToConfiguredMinVersion: [String: Version]) { + self.platformToConfiguredMinVersion = platformToConfiguredMinVersion + super.init(viewMode: .sourceAccurate) + } + + override func visitPost(_ node: AttributeSyntax) { + guard let argument = node.argument?.as(AvailabilitySpecListSyntax.self) else { + return + } + + for arg in argument { + guard let entry = arg.entry.as(AvailabilityVersionRestrictionSyntax.self), + let versionString = entry.version?.description, + case let platform = entry.platform, + let reason = reason(platform: platform, version: versionString, violationType: .attribute) else { + continue + } + + violationPositions.append((node.atSignToken.positionAfterSkippingLeadingTrivia, reason)) } } - var requiredSwiftVersion: SwiftVersion { - switch self { - case .condition, .attribute: - return .five - case .negativeCondition: - return .fiveDotSix + override func visitPost(_ node: UnavailabilityConditionSyntax) { + for elem in node.availabilitySpec { + guard let restriction = elem.entry.as(AvailabilityVersionRestrictionSyntax.self), + let versionString = restriction.version?.description, + let reason = reason(platform: restriction.platform, version: versionString, + violationType: .negativeCondition) else { + continue + } + + violationPositions.append((node.poundUnavailableKeyword.positionAfterSkippingLeadingTrivia, reason)) + } + } + + override func visitPost(_ node: AvailabilityConditionSyntax) { + for elem in node.availabilitySpec { + guard let restriction = elem.entry.as(AvailabilityVersionRestrictionSyntax.self), + let versionString = restriction.version?.description, + let reason = reason(platform: restriction.platform, version: versionString, + violationType: .condition) else { + continue + } + + violationPositions.append((node.poundAvailableKeyword.positionAfterSkippingLeadingTrivia, reason)) + } + } + + private func reason(platform: TokenSyntax, + version versionString: String, + violationType: AvailabilityType) -> String? { + guard let platform = DeploymentTargetConfiguration.Platform(rawValue: platform.text), + let minVersion = platformToConfiguredMinVersion[platform.rawValue] else { + return nil } + + guard let version = try? Version(platform: platform, rawValue: versionString), + version <= minVersion else { + return nil + } + + return """ + Availability \(violationType.displayString) is using a version (\(versionString)) that is \ + satisfied by the deployment target (\(minVersion.stringValue)) for platform \(platform.rawValue). + """ } } } diff --git a/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRuleExamples.swift b/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRuleExamples.swift index 3dbce7faa8..a22c0c8c3d 100644 --- a/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRuleExamples.swift +++ b/Source/SwiftLintFramework/Rules/Lint/DeploymentTargetRuleExamples.swift @@ -1,55 +1,37 @@ internal enum DeploymentTargetRuleExamples { - static let nonTriggeringExamples: [Example] = { - let commonExamples = [ - Example("@available(iOS 12.0, *)\nclass A {}"), - Example("@available(iOSApplicationExtension 13.0, *)\nclass A {}"), - Example("@available(watchOS 4.0, *)\nclass A {}"), - Example("@available(watchOSApplicationExtension 4.0, *)\nclass A {}"), - Example("@available(swift 3.0.2)\nclass A {}"), - Example("class A {}"), - Example("if #available(iOS 10.0, *) {}"), - Example("if #available(iOS 10, *) {}"), - Example("guard #available(iOS 12.0, *) else { return }") - ] + static let nonTriggeringExamples: [Example] = [ + Example("@available(iOS 12.0, *)\nclass A {}"), + Example("@available(iOSApplicationExtension 13.0, *)\nclass A {}"), + Example("@available(watchOS 4.0, *)\nclass A {}"), + Example("@available(watchOSApplicationExtension 4.0, *)\nclass A {}"), + Example("@available(swift 3.0.2)\nclass A {}"), + Example("class A {}"), + Example("if #available(iOS 10.0, *) {}"), + Example("if #available(iOS 10, *) {}"), + Example("guard #available(iOS 12.0, *) else { return }"), + Example("#if #unavailable(iOS 15.0) {}"), + Example("#guard #unavailable(iOS 15.0) {} else { return }") + ] - guard SwiftVersion.current >= .fiveDotSix else { - return commonExamples - } - - return commonExamples + [ - Example("#if #unavailable(iOS 15.0) {}"), - Example("#guard #unavailable(iOS 15.0) {} else { return }") - ] - }() - - static let triggeringExamples: [Example] = { - let commonExamples = [ - Example("↓@available(iOS 6.0, *)\nclass A {}"), - Example("↓@available(iOSApplicationExtension 6.0, *)\nclass A {}"), - Example("↓@available(iOS 7.0, *)\nclass A {}"), - Example("↓@available(iOS 6, *)\nclass A {}"), - Example("↓@available(iOS 6.0, macOS 10.12, *)\n class A {}"), - Example("↓@available(macOS 10.12, iOS 6.0, *)\n class A {}"), - Example("↓@available(macOS 10.7, *)\nclass A {}"), - Example("↓@available(macOSApplicationExtension 10.7, *)\nclass A {}"), - Example("↓@available(OSX 10.7, *)\nclass A {}"), - Example("↓@available(watchOS 0.9, *)\nclass A {}"), - Example("↓@available(watchOSApplicationExtension 0.9, *)\nclass A {}"), - Example("↓@available(tvOS 8, *)\nclass A {}"), - Example("↓@available(tvOSApplicationExtension 8, *)\nclass A {}"), - Example("if ↓#available(iOS 6.0, *) {}"), - Example("if ↓#available(iOS 6, *) {}"), - Example("guard ↓#available(iOS 6.0, *) else { return }") - ] - - guard SwiftVersion.current >= .fiveDotSix else { - return commonExamples - } - - return commonExamples + [ - Example("if ↓#unavailable(iOS 7.0) {}"), - Example("if ↓#unavailable(iOS 6.9) {}"), - Example("guard ↓#unavailable(iOS 7.0) {} else { return }") - ] - }() + static let triggeringExamples: [Example] = [ + Example("↓@available(iOS 6.0, *)\nclass A {}"), + Example("↓@available(iOSApplicationExtension 6.0, *)\nclass A {}"), + Example("↓@available(iOS 7.0, *)\nclass A {}"), + Example("↓@available(iOS 6, *)\nclass A {}"), + Example("↓@available(iOS 6.0, macOS 10.12, *)\n class A {}"), + Example("↓@available(macOS 10.12, iOS 6.0, *)\n class A {}"), + Example("↓@available(macOS 10.7, *)\nclass A {}"), + Example("↓@available(macOSApplicationExtension 10.7, *)\nclass A {}"), + Example("↓@available(OSX 10.7, *)\nclass A {}"), + Example("↓@available(watchOS 0.9, *)\nclass A {}"), + Example("↓@available(watchOSApplicationExtension 0.9, *)\nclass A {}"), + Example("↓@available(tvOS 8, *)\nclass A {}"), + Example("↓@available(tvOSApplicationExtension 8, *)\nclass A {}"), + Example("if ↓#available(iOS 6.0, *) {}"), + Example("if ↓#available(iOS 6, *) {}"), + Example("guard ↓#available(iOS 6.0, *) else { return }"), + Example("if ↓#unavailable(iOS 7.0) {}"), + Example("if ↓#unavailable(iOS 6.9) {}"), + Example("guard ↓#unavailable(iOS 7.0) {} else { return }") + ] }