diff --git a/Source/SwiftLintFramework/Rules/Style/ComputedAccessorsOrderRule.swift b/Source/SwiftLintFramework/Rules/Style/ComputedAccessorsOrderRule.swift index d1836782c..751c2c15d 100644 --- a/Source/SwiftLintFramework/Rules/Style/ComputedAccessorsOrderRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/ComputedAccessorsOrderRule.swift @@ -1,7 +1,6 @@ -import Foundation -import SourceKittenFramework +import SwiftSyntax -public struct ComputedAccessorsOrderRule: ConfigurationProviderRule { +public struct ComputedAccessorsOrderRule: ConfigurationProviderRule, SourceKitFreeRule { public var configuration = ComputedAccessorsOrderRuleConfiguration() public init() {} @@ -16,116 +15,71 @@ public struct ComputedAccessorsOrderRule: ConfigurationProviderRule { ) public func validate(file: SwiftLintFile) -> [StyleViolation] { - let getTokens = findKeywordTokens(keyword: "get", file: file) - - let violatingLocations = getTokens.compactMap { getToken -> (ByteCount, SwiftDeclarationKind?)? in - // the last element is the deepest structure - guard let dict = declarations(forByteOffset: getToken.offset, - structureDictionary: file.structureDictionary).last else { - return nil + ComputedAccessorsOrderRuleVisitor(expectedOrder: configuration.order) + .walk(file: file, handler: \.violationPositions) + .sorted { $0.position < $1.position } + .map { violation in + StyleViolation( + ruleDescription: Self.description, + severity: configuration.severityConfiguration.severity, + location: Location(file: file, position: violation.position), + reason: reason(for: violation.kind) + ) } - - guard let range = dict.byteRange.map(file.stringView.byteRangeToNSRange) else { - return nil - } - - let setTokens = findKeywordTokens(keyword: "set", file: file, range: range) - let setToken = setTokens.first { token in - // the last element is the deepest structure - guard let setDict = declarations(forByteOffset: token.offset, - structureDictionary: file.structureDictionary).last else { - return false - } - - return setDict.offset == dict.offset - } - - let tokensInOrder = [getToken, setToken].compactMap { $0?.offset }.sorted() - let expectedOrder: [ByteCount] - switch configuration.order { - case .getSet: - expectedOrder = [getToken, setToken].compactMap { $0?.offset } - case .setGet: - expectedOrder = [setToken, getToken].compactMap { $0?.offset } - } - - guard tokensInOrder != expectedOrder else { - return nil - } - - let kind = dict.declarationKind - return (tokensInOrder[0], kind) - } - - return violatingLocations.map { offset, kind in - let reason = kind.map { kind -> String in - let kindString = kind == .functionSubscript ? "subscripts" : "properties" - let orderString: String - switch configuration.order { - case .getSet: - orderString = "getter and then the setter" - case .setGet: - orderString = "setter and then the getter" - } - return "Computed \(kindString) should declare first the \(orderString)." - } - - return StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: Location(file: file, byteOffset: offset), - reason: reason) - } } - private func findKeywordTokens(keyword: String, - file: SwiftLintFile, - range: NSRange? = nil) -> [SwiftLintSyntaxToken] { - let pattern = "\\b\(keyword)\\b" - return file.rangesAndTokens(matching: pattern, range: range).compactMap { _, tokens in - guard tokens.count == 1, - let token = tokens.last, - token.kind == .keyword else { - return nil - } - - return token + private func reason(for kind: ComputedAccessorsOrderRuleVisitor.ViolationKind) -> String { + let kindString = kind == .subscript ? "subscripts" : "properties" + let orderString: String + switch configuration.order { + case .getSet: + orderString = "getter and then the setter" + case .setGet: + orderString = "setter and then the getter" } + return "Computed \(kindString) should declare first the \(orderString)." } } -private extension ComputedAccessorsOrderRule { - func declarations(forByteOffset byteOffset: ByteCount, - structureDictionary: SourceKittenDictionary) -> [SourceKittenDictionary] { - var results = [SourceKittenDictionary]() - let allowedKinds = SwiftDeclarationKind.variableKinds.subtracting([.varParameter]) - .union([.functionSubscript]) +private final class ComputedAccessorsOrderRuleVisitor: SyntaxVisitor { + enum ViolationKind { + case `subscript`, property + } - func parse(dictionary: SourceKittenDictionary, - parentKind: SwiftDeclarationKind?, - into results: inout [SourceKittenDictionary]) { - // Only accepts declarations which contains a body and contains the - // searched byteOffset - guard let kind = dictionary.declarationKind, - let byteRange = dictionary.byteRange, - byteRange.contains(byteOffset) - else { - return - } + private(set) var violationPositions: [(position: AbsolutePosition, kind: ViolationKind)] = [] + private let expectedOrder: ComputedAccessorsOrderRuleConfiguration.Order - if parentKind != .protocol && allowedKinds.contains(kind) { - results.append(dictionary) - } + init(expectedOrder: ComputedAccessorsOrderRuleConfiguration.Order) { + self.expectedOrder = expectedOrder + } - for dictionary in dictionary.substructure { - parse(dictionary: dictionary, parentKind: kind, into: &results) - } + override func visitPost(_ node: AccessorBlockSyntax) { + guard let firstAccessor = node.accessors.first, + let order = node.order, + order != expectedOrder else { + return } - let dict = structureDictionary - for dictionary in dict.substructure { - parse(dictionary: dictionary, parentKind: nil, into: &results) - } - - return results + let kind: ViolationKind = node.parent?.as(SubscriptDeclSyntax.self) == nil ? .property : .subscript + violationPositions.append((firstAccessor.positionAfterSkippingLeadingTrivia, kind)) + } +} + +private extension AccessorBlockSyntax { + var order: ComputedAccessorsOrderRuleConfiguration.Order? { + guard accessors.count == 2, accessors.map(\.body).allSatisfy({ $0 != nil }) else { + return nil + } + + let tokens = accessors.map(\.accessorKind.tokenKind) + if tokens == [.contextualKeyword("get"), .contextualKeyword("set")] { + return .getSet + } + + if tokens == [.contextualKeyword("set"), .contextualKeyword("get")] { + return .setGet + } + + return nil } }