SwiftLint/Source/SwiftLintBuiltInRules/Rules/Style/ModifierOrderRule.swift

185 lines
7.5 KiB
Swift

import Foundation
import SourceKittenFramework
struct ModifierOrderRule: ASTRule, OptInRule, ConfigurationProviderRule, CorrectableRule {
var configuration = ModifierOrderConfiguration()
static let description = RuleDescription(
identifier: "modifier_order",
name: "Modifier Order",
description: "Modifier order should be consistent.",
kind: .style,
nonTriggeringExamples: ModifierOrderRuleExamples.nonTriggeringExamples,
triggeringExamples: ModifierOrderRuleExamples.triggeringExamples
)
func validate(file: SwiftLintFile,
kind: SwiftDeclarationKind,
dictionary: SourceKittenDictionary) -> [StyleViolation] {
guard let offset = dictionary.offset else {
return []
}
let violatingModifiers = self.violatingModifiers(dictionary: dictionary)
if let first = violatingModifiers.first {
let preferredModifier = first.0
let declaredModifier = first.1
let reason = "\(preferredModifier.keyword) modifier should come before \(declaredModifier.keyword)"
return [
StyleViolation(
ruleDescription: Self.description,
severity: configuration.severityConfiguration.severity,
location: Location(file: file, byteOffset: offset),
reason: reason
)
]
} else {
return []
}
}
func correct(file: SwiftLintFile) -> [Correction] {
return file.structureDictionary.traverseDepthFirst { subDict in
guard subDict.declarationKind != nil else { return nil }
return correct(file: file, dictionary: subDict)
}
}
private func correct(file: SwiftLintFile, dictionary: SourceKittenDictionary) -> [Correction] {
guard let offset = dictionary.offset else { return [] }
let originalContents = file.stringView
let violatingRanges = violatingModifiers(dictionary: dictionary)
.compactMap { preferred, declared -> (NSRange, NSRange)? in
guard
let preferredRange = originalContents.byteRangeToNSRange(
preferred.range
).flatMap({ file.ruleEnabled(violatingRange: $0, for: self) }),
let declaredRange = originalContents.byteRangeToNSRange(
declared.range
).flatMap({ file.ruleEnabled(violatingRange: $0, for: self) })
else {
return nil
}
return (preferredRange, declaredRange)
}
let corrections: [Correction]
if violatingRanges.isEmpty {
corrections = []
} else {
var correctedContents = originalContents.nsString
violatingRanges.reversed().forEach { arg in
let (preferredModifierRange, declaredModifierRange) = arg
correctedContents = correctedContents.replacingCharacters(
in: declaredModifierRange,
with: originalContents.substring(with: preferredModifierRange)
).bridge()
}
file.write(correctedContents.bridge())
corrections = [
Correction(
ruleDescription: Self.description,
location: Location(
file: file,
byteOffset: offset
)
)
]
}
return corrections
}
private func violatableModifiers(declaredModifiers: [ModifierDescription]) -> [ModifierDescription] {
let preferredModifierGroups = ([.atPrefixed] + configuration.preferredModifierOrder)
return declaredModifiers.filter { preferredModifierGroups.contains($0.group) }
}
private func prioritizedModifiers(
violatableModifiers: [ModifierDescription]
) -> [(priority: Int, modifier: ModifierDescription)] {
let prioritizedPreferredModifierGroups = ([.atPrefixed] + configuration.preferredModifierOrder).enumerated()
return violatableModifiers.reduce(
into: [(priority: Int, modifier: ModifierDescription)]()
) { prioritizedModifiers, modifier in
guard let priority = prioritizedPreferredModifierGroups.first(
where: { _, group in modifier.group == group }
)?.offset else {
return
}
prioritizedModifiers.append((priority: priority, modifier: modifier))
}
}
private func violatingModifiers(
dictionary: SourceKittenDictionary
) -> [(preferredModifier: ModifierDescription, declaredModifier: ModifierDescription)] {
let violatableModifiers = self.violatableModifiers(declaredModifiers: dictionary.modifierDescriptions)
let prioritizedModifiers = self.prioritizedModifiers(violatableModifiers: violatableModifiers)
let sortedByPriorityModifiers = prioritizedModifiers
.sorted { $0.priority < $1.priority }
.map { $0.modifier }
return zip(sortedByPriorityModifiers, violatableModifiers).filter { $0 != $1 }
}
}
private extension SourceKittenDictionary {
var modifierDescriptions: [ModifierDescription] {
let staticKinds = [SwiftDeclarationKind.functionMethodClass, .functionMethodStatic, .varClass, .varStatic]
let staticKindsAndOffsets = kindsAndOffsets(in: staticKinds).map { [$0] } ?? []
return (swiftAttributes + staticKindsAndOffsets)
.sorted {
guard let rhsOffset = $0.offset, let lhsOffset = $1.offset else {
return false
}
return rhsOffset < lhsOffset
}
.compactMap {
guard let offset = $0.offset else { return nil }
if let attribute = $0.attribute,
let modifierGroup = SwiftDeclarationAttributeKind.ModifierGroup(rawAttribute: attribute),
let length = $0.length {
return ModifierDescription(
keyword: attribute.lastComponentAfter("."),
group: modifierGroup,
offset: offset,
length: length
)
} else if let kind = $0.kind {
let keyword = kind.lastComponentAfter(".")
return ModifierDescription(
keyword: keyword,
group: .typeMethods,
offset: offset,
length: ByteCount(keyword.lengthOfBytes(using: .utf8))
)
}
return nil
}
}
private func kindsAndOffsets(in declarationKinds: [SwiftDeclarationKind]) -> SourceKittenDictionary? {
guard let offset, let declarationKind, declarationKinds.contains(declarationKind) else {
return nil
}
return SourceKittenDictionary(["key.kind": declarationKind.rawValue, "key.offset": Int64(offset.value)])
}
}
private extension String {
func lastComponentAfter(_ character: String) -> String {
return components(separatedBy: character).last ?? ""
}
}
private struct ModifierDescription: Equatable {
let keyword: String
let group: SwiftDeclarationAttributeKind.ModifierGroup
let offset: ByteCount
let length: ByteCount
var range: ByteRange { return ByteRange(location: offset, length: length) }
}