File: FileScopedDeclarationPrivacy.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 (168 lines) | stat: -rw-r--r-- 6,446 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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 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

/// Declarations at file scope with effective private access should be consistently declared as
/// either `fileprivate` or `private`, determined by configuration.
///
/// Lint: If a file-scoped declaration has formal access opposite to the desired access level in the
///       formatter's configuration, a lint error is raised.
///
/// Format: File-scoped declarations that have formal access opposite to the desired access level in
///         the formatter's configuration will have their access level changed.
@_spi(Rules)
public final class FileScopedDeclarationPrivacy: SyntaxFormatRule {
  public override func visit(_ node: SourceFileSyntax) -> SourceFileSyntax {
    var result = node
    result.statements = rewrittenCodeBlockItems(node.statements)
    return result
  }

  /// Returns a list of code block items equivalent to the given list, but where any file-scoped
  /// declarations with effective private access have had their formal access level rewritten, if
  /// necessary, to be either `private` or `fileprivate`, as determined by the formatter
  /// configuration.
  ///
  /// - Parameter codeBlockItems: The list of code block items to rewrite.
  /// - Returns: A new `CodeBlockItemListSyntax` that has possibly been rewritten.
  private func rewrittenCodeBlockItems(_ codeBlockItems: CodeBlockItemListSyntax)
    -> CodeBlockItemListSyntax
  {
    let newCodeBlockItems = codeBlockItems.map { codeBlockItem -> CodeBlockItemSyntax in
      switch codeBlockItem.item {
      case .decl(let decl):
        var result = codeBlockItem
        result.item = .decl(rewrittenDecl(decl))
        return result
      default:
        return codeBlockItem
      }
    }
    return CodeBlockItemListSyntax(newCodeBlockItems)
  }

  private func rewrittenDecl(_ decl: DeclSyntax) -> DeclSyntax {
    switch Syntax(decl).as(SyntaxEnum.self) {
    case .ifConfigDecl(let ifConfigDecl):
      // We need to look through `#if/#elseif/#else` blocks because the decls directly inside
      // them are still considered file-scope for our purposes.
      return DeclSyntax(rewrittenIfConfigDecl(ifConfigDecl))

    case .functionDecl(let functionDecl):
      return DeclSyntax(rewrittenDecl(functionDecl))

    case .variableDecl(let variableDecl):
      return DeclSyntax(rewrittenDecl(variableDecl))

    case .classDecl(let classDecl):
      return DeclSyntax(rewrittenDecl(classDecl))

    case .structDecl(let structDecl):
      return DeclSyntax(rewrittenDecl(structDecl))

    case .enumDecl(let enumDecl):
      return DeclSyntax(rewrittenDecl(enumDecl))

    case .protocolDecl(let protocolDecl):
      return DeclSyntax(rewrittenDecl(protocolDecl))

    case .typeAliasDecl(let typealiasDecl):
      return DeclSyntax(rewrittenDecl(typealiasDecl))

    default:
      return decl
    }
  }

  /// Returns a new `IfConfigDeclSyntax` equivalent to the given node, but where any file-scoped
  /// declarations with effective private access have had their formal access level rewritten, if
  /// necessary, to be either `private` or `fileprivate`, as determined by the formatter
  /// configuration.
  ///
  /// - Parameter ifConfigDecl: The `IfConfigDeclSyntax` to rewrite.
  /// - Returns: A new `IfConfigDeclSyntax` that has possibly been rewritten.
  private func rewrittenIfConfigDecl(_ ifConfigDecl: IfConfigDeclSyntax) -> IfConfigDeclSyntax {
    let newClauses = ifConfigDecl.clauses.map { clause -> IfConfigClauseSyntax in
      switch clause.elements {
      case .statements(let codeBlockItemList)?:
        var result = clause
        result.elements = .statements(rewrittenCodeBlockItems(codeBlockItemList))
        return result
      default:
        return clause
      }
    }

    var result = ifConfigDecl
    result.clauses = IfConfigClauseListSyntax(newClauses)
    return result
  }

  /// Returns a rewritten version of the given declaration if its modifier list contains `private`
  /// that contains `fileprivate` instead.
  ///
  /// If the modifier list is not inconsistent with the configured access level, the original
  /// declaration is returned unchanged.
  ///
  /// - Parameters:
  ///   - decl: The declaration to possibly rewrite.
  ///   - modifiers: The modifier list of the declaration (i.e., `decl.modifiers`).
  ///   - factory: A reference to the `decl`'s `withModifiers` instance method that is called to
  ///     rewrite the node if needed.
  /// - Returns: A new node if the modifiers were rewritten, or the original node if not.
  private func rewrittenDecl<DeclType: DeclSyntaxProtocol & WithModifiersSyntax>(
    _ decl: DeclType
  ) -> DeclType {
    let invalidAccess: Keyword
    let validAccess: Keyword
    let diagnostic: Finding.Message

    switch context.configuration.fileScopedDeclarationPrivacy.accessLevel {
    case .private:
      invalidAccess = .fileprivate
      validAccess = .private
      diagnostic = .replaceFileprivateWithPrivate
    case .fileprivate:
      invalidAccess = .private
      validAccess = .fileprivate
      diagnostic = .replacePrivateWithFileprivate
    }

    guard decl.modifiers.contains(anyOf: [invalidAccess]) else {
      return decl
    }

    let newModifiers = decl.modifiers.map { modifier -> DeclModifierSyntax in
      var modifier = modifier

      let name = modifier.name
      if case .keyword(invalidAccess) = name.tokenKind {
        diagnose(diagnostic, on: name)
        modifier.name.tokenKind = .keyword(validAccess)
      }
      return modifier
    }

    var result = decl
    result.modifiers = DeclModifierListSyntax(newModifiers)
    return result
  }
}

extension Finding.Message {
  fileprivate static let replacePrivateWithFileprivate: Finding.Message =
    "replace 'private' with 'fileprivate' on file-scoped declarations"

  fileprivate static let replaceFileprivateWithPrivate: Finding.Message =
    "replace 'fileprivate' with 'private' on file-scoped declarations"
}