diff --git a/CHANGELOG.md b/CHANGELOG.md index ecf27194d..172cbf65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - `closing_brace` - `closure_body_length` - `closure_parameter_position` + - `collection_alignment` - `comment_spacing` - `computed_accessors_order` - `conditional_returns_on_newline` diff --git a/Source/SwiftLintFramework/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift b/Source/SwiftLintFramework/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift index 0680700b6..74f6183a5 100644 --- a/Source/SwiftLintFramework/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift +++ b/Source/SwiftLintFramework/Rules/RuleConfigurations/CollectionAlignmentConfiguration.swift @@ -1,8 +1,8 @@ -public struct CollectionAlignmentConfiguration: RuleConfiguration, Equatable { - private(set) var severityConfiguration = SeverityConfiguration(.warning) - private(set) var alignColons = false +public struct CollectionAlignmentConfiguration: SeverityBasedRuleConfiguration, Equatable { + public private(set) var severityConfiguration = SeverityConfiguration(.warning) + public private(set) var alignColons = false - init() {} + public init() {} public var consoleDescription: String { return severityConfiguration.consoleDescription + ", align_colons: \(alignColons)" diff --git a/Source/SwiftLintFramework/Rules/Style/CollectionAlignmentRule.swift b/Source/SwiftLintFramework/Rules/Style/CollectionAlignmentRule.swift index 2f4d7b696..89703bd1f 100644 --- a/Source/SwiftLintFramework/Rules/Style/CollectionAlignmentRule.swift +++ b/Source/SwiftLintFramework/Rules/Style/CollectionAlignmentRule.swift @@ -1,6 +1,6 @@ -import SourceKittenFramework +import SwiftSyntax -public struct CollectionAlignmentRule: ASTRule, ConfigurationProviderRule, OptInRule { +public struct CollectionAlignmentRule: SwiftSyntaxRule, ConfigurationProviderRule, OptInRule { public var configuration = CollectionAlignmentConfiguration() public init() {} @@ -14,89 +14,62 @@ public struct CollectionAlignmentRule: ASTRule, ConfigurationProviderRule, OptIn triggeringExamples: Examples(alignColons: false).triggeringExamples ) - public func validate(file: SwiftLintFile, kind: SwiftExpressionKind, - dictionary: SourceKittenDictionary) -> [StyleViolation] { - guard kind == .dictionary || kind == .array else { return [] } + public func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor { + Visitor(alignColons: configuration.alignColons, locationConverter: file.locationConverter) + } +} - let keyLocations: [Location] - if kind == .array { - keyLocations = arrayElementLocations(with: file, dictionary: dictionary) - } else { - keyLocations = dictionaryKeyLocations(with: file, dictionary: dictionary) +private extension CollectionAlignmentRule { + final class Visitor: ViolationsSyntaxVisitor { + private let alignColons: Bool + private let locationConverter: SourceLocationConverter + + init(alignColons: Bool, locationConverter: SourceLocationConverter) { + self.alignColons = alignColons + self.locationConverter = locationConverter + super.init(viewMode: .sourceAccurate) } - guard keyLocations.count >= 2 else { - return [] + override func visitPost(_ node: ArrayExprSyntax) { + let locations = node.elements.map { element in + locationConverter.location(for: element.positionAfterSkippingLeadingTrivia) + } + violations.append(contentsOf: validate(keyLocations: locations)) } - let firstKeyLocation = keyLocations[0] - let remainingKeyLocations = keyLocations[1...] - let violationLocations = zip(remainingKeyLocations.indices, remainingKeyLocations) - .compactMap { index, location -> Location? in - let previousLocation = keyLocations[index - 1] - guard let previousLine = previousLocation.line, - let locationLine = location.line, - let firstKeyCharacter = firstKeyLocation.character, - let locationCharacter = location.character, - previousLine < locationLine, - firstKeyCharacter != locationCharacter else { return nil } + override func visitPost(_ node: DictionaryElementListSyntax) { + let locations = node.map { element in + let position = alignColons ? element.colon.positionAfterSkippingLeadingTrivia : + element.keyExpression.positionAfterSkippingLeadingTrivia + return locationConverter.location(for: position) + } + violations.append(contentsOf: validate(keyLocations: locations)) + } - return location + private func validate(keyLocations: [SourceLocation]) -> [AbsolutePosition] { + guard keyLocations.count >= 2 else { + return [] } - return violationLocations.map { - StyleViolation(ruleDescription: Self.description, - severity: configuration.severityConfiguration.severity, - location: $0) + let firstKeyLocation = keyLocations[0] + let remainingKeyLocations = keyLocations[1...] + + return zip(remainingKeyLocations.indices, remainingKeyLocations) + .compactMap { index, location -> AbsolutePosition? in + let previousLocation = keyLocations[index - 1] + guard let previousLine = previousLocation.line, + let locationLine = location.line, + let firstKeyColumn = firstKeyLocation.column, + let locationColumn = location.column, + previousLine < locationLine, + firstKeyColumn != locationColumn else { + return nil + } + + return locationConverter.position(ofLine: locationLine, column: locationColumn) + } } } - - private func arrayElementLocations(with file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [Location] { - return dictionary.elements.compactMap { element -> Location? in - element.offset.map { Location(file: file, byteOffset: $0) } - } - } - - private func dictionaryKeyLocations(with file: SwiftLintFile, - dictionary: SourceKittenDictionary) -> [Location] { - var keys: [SourceKittenDictionary] = [] - var values: [SourceKittenDictionary] = [] - dictionary.elements.enumerated().forEach { index, element in - // in a dictionary, the even elements are keys, and the odd elements are values - if index.isMultiple(of: 2) { - keys.append(element) - } else { - values.append(element) - } - } - - return zip(keys, values).compactMap { key, value -> Location? in - guard let keyOffset = key.offset, - let valueOffset = value.offset, - let keyLength = key.length else { return nil } - - if configuration.alignColons { - return colonLocation(with: file, - keyOffset: keyOffset, - keyLength: keyLength, - valueOffset: valueOffset) - } else { - return Location(file: file, byteOffset: keyOffset) - } - } - } - - private func colonLocation(with file: SwiftLintFile, keyOffset: ByteCount, keyLength: ByteCount, - valueOffset: ByteCount) -> Location? { - let contents = file.stringView - let matchStart = keyOffset + keyLength - let matchLength = valueOffset - matchStart - let byteRange = ByteRange(location: matchStart, length: matchLength) - let range = contents.byteRangeToNSRange(byteRange) - - let matches = file.match(pattern: ":", excludingSyntaxKinds: [.comment], range: range) - return matches.first.map { Location(file: file, characterOffset: $0.location) } - } } extension CollectionAlignmentRule {