File: FullyIndirectEnum.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (131 lines) | stat: -rw-r--r-- 4,895 bytes parent folder | download
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"
}