1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
import SwiftSyntax
/// If all cases of an enum are `indirect`, the entire enum should be marked `indirect`.
///
/// Lint: If every case of an enum is `indirect`, but the enum itself is not, a lint error is
/// raised.
///
/// Format: Enums where all cases are `indirect` will be rewritten such that the enum is marked
/// `indirect`, and each case is not.
@_spi(Rules)
public final class FullyIndirectEnum: SyntaxFormatRule {
public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
let enumMembers = node.memberBlock.members
guard !node.modifiers.contains(anyOf: [.indirect]),
case let indirectModifiers = indirectModifiersIfAllCasesIndirect(in: enumMembers),
!indirectModifiers.isEmpty
else {
return DeclSyntax(node)
}
let notes = indirectModifiers.map { modifier in
Finding.Note(
message: .removeIndirect,
location: Finding.Location(
modifier.startLocation(converter: self.context.sourceLocationConverter)))
}
diagnose(
.moveIndirectKeywordToEnumDecl(name: node.name.text), on: node.enumKeyword, notes: notes)
// Removes 'indirect' keyword from cases, reformats
let newMembers = enumMembers.map {
(member: MemberBlockItemSyntax) -> MemberBlockItemSyntax in
guard let caseMember = member.decl.as(EnumCaseDeclSyntax.self),
caseMember.modifiers.contains(anyOf: [.indirect]),
let firstModifier = caseMember.modifiers.first
else {
return member
}
var newCase = caseMember
newCase.modifiers.remove(anyOf: [.indirect])
var newMember = member
newMember.decl = DeclSyntax(rearrangeLeadingTrivia(firstModifier.leadingTrivia, on: newCase))
return newMember
}
// If the `indirect` keyword being added would be the first token in the decl, we need to move
// the leading trivia from the `enum` keyword to the new modifier to preserve the existing
// line breaks/comments/indentation.
let firstTok = node.firstToken(viewMode: .sourceAccurate)!
let leadingTrivia: Trivia
var newEnumDecl = node
if firstTok.tokenKind == .keyword(.enum) {
leadingTrivia = firstTok.leadingTrivia
newEnumDecl.leadingTrivia = []
} else {
leadingTrivia = []
}
let newModifier = DeclModifierSyntax(
name: TokenSyntax.identifier(
"indirect", leadingTrivia: leadingTrivia, trailingTrivia: .spaces(1)), detail: nil)
newEnumDecl.modifiers = newEnumDecl.modifiers + [newModifier]
newEnumDecl.memberBlock.members = MemberBlockItemListSyntax(newMembers)
return DeclSyntax(newEnumDecl)
}
/// Returns a value indicating whether all enum cases in the given list are indirect.
///
/// Note that if the enum has no cases, this returns false.
private func indirectModifiersIfAllCasesIndirect(in members: MemberBlockItemListSyntax)
-> [DeclModifierSyntax]
{
var indirectModifiers = [DeclModifierSyntax]()
for member in members {
if let caseMember = member.decl.as(EnumCaseDeclSyntax.self) {
guard let indirectModifier = caseMember.modifiers.first(
where: { $0.name.text == "indirect" }
) else {
return []
}
indirectModifiers.append(indirectModifier)
}
}
return indirectModifiers
}
/// Transfers given leading trivia to the first token in the case declaration.
private func rearrangeLeadingTrivia(
_ leadingTrivia: Trivia,
on enumCaseDecl: EnumCaseDeclSyntax
) -> EnumCaseDeclSyntax {
var formattedCase = enumCaseDecl
if var firstModifier = formattedCase.modifiers.first {
// If the case has modifiers, attach the leading trivia to the first one.
firstModifier.leadingTrivia = leadingTrivia
formattedCase.modifiers[formattedCase.modifiers.startIndex] = firstModifier
formattedCase.modifiers = formattedCase.modifiers
} else {
// Otherwise, attach the trivia to the `case` keyword itself.
formattedCase.caseKeyword.leadingTrivia = leadingTrivia
}
return formattedCase
}
}
extension Finding.Message {
fileprivate static func moveIndirectKeywordToEnumDecl(name: String) -> Finding.Message {
"declare enum '\(name)' itself as indirect when all cases are indirect"
}
fileprivate static let removeIndirect: Finding.Message = "remove 'indirect' here"
}
|