mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
182 lines
6.6 KiB
Swift
182 lines
6.6 KiB
Swift
import SourceKittenFramework
|
|
import SwiftLintCore
|
|
import SwiftSyntax
|
|
|
|
@SwiftSyntaxRule(explicitRewriter: true, optIn: true)
|
|
struct ModifierOrderRule: Rule {
|
|
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
|
|
)
|
|
}
|
|
|
|
private extension ModifierOrderRule {
|
|
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
|
|
override func visitPost(_ node: DeclModifierListSyntax) {
|
|
guard let parent = node.parent else {
|
|
return
|
|
}
|
|
|
|
let introducer: TokenSyntax? = parent.asProtocol((any DeclGroupSyntax).self)?.introducer
|
|
?? parent.as(FunctionDeclSyntax.self)?.funcKeyword
|
|
?? parent.as(InitializerDeclSyntax.self)?.initKeyword
|
|
?? parent.as(SubscriptDeclSyntax.self)?.subscriptKeyword
|
|
?? parent.as(VariableDeclSyntax.self)?.bindingSpecifier
|
|
|
|
guard let introducer else {
|
|
return
|
|
}
|
|
|
|
let descriptions = node.modifierDescriptions
|
|
let differences = descriptions
|
|
.bubbleSort(by: configuration.preferredModifierOrder)
|
|
.difference(from: descriptions)
|
|
let orderedDescriptions = descriptions.applying(differences) ?? descriptions
|
|
|
|
if let diff = zip(orderedDescriptions, descriptions).first(where: { $0.keyword != $1.keyword }) {
|
|
violations.append(.init(
|
|
position: introducer.positionAfterSkippingLeadingTrivia,
|
|
reason: "\(diff.0.keyword) modifier should come before \(diff.1.keyword)"
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
|
|
override func visit(_ node: DeclModifierListSyntax) -> DeclModifierListSyntax {
|
|
let modifierDescriptions = node.modifierDescriptions
|
|
let prevModifiers = modifierDescriptions.map(\.modifier)
|
|
let differences = modifierDescriptions
|
|
.bubbleSort(by: configuration.preferredModifierOrder)
|
|
.map(\.modifier)
|
|
.difference(from: prevModifiers)
|
|
if differences.isEmpty {
|
|
return super.visit(node)
|
|
}
|
|
numberOfCorrections += differences.count
|
|
var newModifiers = prevModifiers
|
|
for change in differences {
|
|
switch change {
|
|
case let .remove(offset, _, _):
|
|
newModifiers.remove(at: offset)
|
|
case let .insert(offset, element, _):
|
|
let prevModifier = prevModifiers[offset]
|
|
newModifiers.insert(element.with(\.leadingTrivia, prevModifier.leadingTrivia), at: offset)
|
|
if offset == 0, newModifiers.count > 1 {
|
|
newModifiers[1] = newModifiers[1].with(\.leadingTrivia, [])
|
|
}
|
|
}
|
|
}
|
|
let newNode = DeclModifierListSyntax(newModifiers)
|
|
.with(\.leadingTrivia, node.leadingTrivia)
|
|
.with(\.trailingTrivia, node.trailingTrivia)
|
|
return super.visit(newNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension DeclModifierListSyntax {
|
|
var modifierDescriptions: [ModifierDescription] {
|
|
var descriptions: [ModifierDescription] = []
|
|
|
|
for modifier in self {
|
|
let keyword = modifier.name.text
|
|
let position = modifier.positionAfterSkippingLeadingTrivia
|
|
guard let group = SwiftDeclarationAttributeKind.ModifierGroup(modifierKeyword: keyword) else {
|
|
continue
|
|
}
|
|
|
|
// Handle setter access modifiers like `private(set)``.
|
|
if let detail = modifier.detail?.detail.tokenKind,
|
|
case .identifier(let detailText) = detail, detailText == "set" {
|
|
if case .acl = group {
|
|
descriptions.append(.init(
|
|
keyword: "\(keyword)(set)",
|
|
modifier: modifier,
|
|
group: .setterACL,
|
|
position: position
|
|
))
|
|
}
|
|
continue
|
|
}
|
|
|
|
descriptions.append(.init(
|
|
keyword: keyword,
|
|
modifier: modifier,
|
|
group: group,
|
|
position: position
|
|
))
|
|
}
|
|
|
|
return descriptions
|
|
}
|
|
}
|
|
|
|
private extension SwiftDeclarationAttributeKind.ModifierGroup {
|
|
init?(modifierKeyword: String) { // swiftlint:disable:this cyclomatic_complexity
|
|
switch modifierKeyword {
|
|
case "override":
|
|
self = .override
|
|
case "weak":
|
|
self = .owned
|
|
case "final":
|
|
self = .final
|
|
case "required":
|
|
self = .required
|
|
case "convenience":
|
|
self = .convenience
|
|
case "lazy":
|
|
self = .lazy
|
|
case "dynamic":
|
|
self = .dynamic
|
|
case "nonisolated":
|
|
self = .isolation
|
|
case "private", "fileprivate", "internal", "public", "open":
|
|
self = .acl
|
|
case "mutating", "nonmutating":
|
|
self = .mutators
|
|
case "static", "class":
|
|
self = .typeMethods
|
|
case _ where modifierKeyword.hasPrefix("@"):
|
|
self = .atPrefixed
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct ModifierDescription: Equatable {
|
|
let keyword: String
|
|
let modifier: DeclModifierSyntax
|
|
let group: SwiftDeclarationAttributeKind.ModifierGroup
|
|
let position: AbsolutePosition
|
|
}
|
|
|
|
private extension [ModifierDescription] {
|
|
func bubbleSort(by preferredOrder: [SwiftDeclarationAttributeKind.ModifierGroup]) -> [ModifierDescription] {
|
|
var sorted = Self()
|
|
for element in self {
|
|
var inserted = false
|
|
for (index, sortedElement) in sorted.enumerated() {
|
|
let elementIndex = preferredOrder.firstIndex(of: element.group)
|
|
let sortedElementIndex = preferredOrder.firstIndex(of: sortedElement.group)
|
|
if let elementIndex, let sortedElementIndex, elementIndex < sortedElementIndex {
|
|
sorted.insert(element, at: index)
|
|
inserted = true
|
|
break
|
|
}
|
|
}
|
|
if !inserted {
|
|
sorted.append(element)
|
|
}
|
|
}
|
|
return sorted
|
|
}
|
|
}
|