File: UseSynthesizedInitializer.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 (250 lines) | stat: -rw-r--r-- 9,829 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
//===----------------------------------------------------------------------===//
//
// 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 Foundation
import SwiftSyntax

/// When possible, the synthesized `struct` initializer should be used.
///
/// This means the creation of a (non-public) memberwise initializer with the same structure as the
/// synthesized initializer is forbidden.
///
/// Lint: (Non-public) memberwise initializers with the same structure as the synthesized
///       initializer will yield a lint error.
@_spi(Rules)
public final class UseSynthesizedInitializer: SyntaxLintRule {

  public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
    var storedProperties: [VariableDeclSyntax] = []
    var initializers: [InitializerDeclSyntax] = []

    for memberItem in node.memberBlock.members {
      let member = memberItem.decl
      // Collect all stored variables into a list
      if let varDecl = member.as(VariableDeclSyntax.self) {
        guard !varDecl.modifiers.contains(anyOf: [.static]) else { continue }
        storedProperties.append(varDecl)
        // Collect any possible redundant initializers into a list
      } else if let initDecl = member.as(InitializerDeclSyntax.self) {
        guard initDecl.optionalMark == nil else { continue }
        guard initDecl.signature.effectSpecifiers?.throwsSpecifier == nil else { continue }
        initializers.append(initDecl)
      }
    }

    // Collects all of the initializers that could be replaced by the synthesized memberwise
    // initializer(s).
    var extraneousInitializers = [InitializerDeclSyntax]()
    for initializer in initializers {
      guard
        // Attributes signify intent that isn't automatically synthesized by the compiler.
        initializer.attributes.isEmpty,
        matchesPropertyList(
          parameters: initializer.signature.parameterClause.parameters,
          properties: storedProperties),
        matchesAssignmentBody(
          variables: storedProperties,
          initBody: initializer.body),
        matchesAccessLevel(
          modifiers: initializer.modifiers,
          properties: storedProperties)
      else {
        continue
      }

      extraneousInitializers.append(initializer)
    }

    // The synthesized memberwise initializer(s) are only created when there are no initializers.
    // If there are other initializers that cannot be replaced by a synthesized memberwise
    // initializer, then all of the initializers must remain.
    let initializersCount = node.memberBlock.members.filter { $0.decl.is(InitializerDeclSyntax.self) }.count
    if extraneousInitializers.count == initializersCount {
      extraneousInitializers.forEach { diagnose(.removeRedundantInitializer, on: $0) }
    }

    return .skipChildren
  }

  /// Compares the actual access level of an initializer with the access level of a synthesized
  /// memberwise initializer.
  ///
  /// - Parameters:
  ///   - modifiers: The modifier list from the initializer.
  ///   - properties: The properties from the enclosing type.
  /// - Returns: Whether the initializer has the same access level as the synthesized initializer.
  private func matchesAccessLevel(
    modifiers: DeclModifierListSyntax?, properties: [VariableDeclSyntax]
  ) -> Bool {
    let synthesizedAccessLevel = synthesizedInitAccessLevel(using: properties)
    let accessLevel = modifiers?.accessLevelModifier
    switch synthesizedAccessLevel {
    case .internal:
      // No explicit access level or internal are equivalent.
      return accessLevel == nil || accessLevel!.name.tokenKind == .keyword(.internal)
    case .fileprivate:
      return accessLevel != nil && accessLevel!.name.tokenKind == .keyword(.fileprivate)
    case .private:
      return accessLevel != nil && accessLevel!.name.tokenKind == .keyword(.private)
    }
  }

  // Compares initializer parameters to stored properties of the struct
  private func matchesPropertyList(
    parameters: FunctionParameterListSyntax,
    properties: [VariableDeclSyntax]
  ) -> Bool {
    guard parameters.count == properties.count else { return false }
    for (idx, parameter) in parameters.enumerated() {

      guard parameter.secondName == nil else { return false }

      let property = properties[idx]
      let propertyId = property.firstIdentifier
      guard let propertyType = property.firstType else { return false }

      // Ensure that parameters that correspond to properties declared using 'var' have a default
      // argument that is identical to the property's default value. Otherwise, a default argument
      // doesn't match the memberwise initializer.
      let isVarDecl = property.bindingSpecifier.tokenKind == .keyword(.var)
      if isVarDecl, let initializer = property.firstInitializer {
        guard let defaultArg = parameter.defaultValue else { return false }
        guard initializer.value.description == defaultArg.value.description else { return false }
      } else if parameter.defaultValue != nil {
        return false
      }

      if propertyId.identifier.text != parameter.firstName.text
        || propertyType.description.trimmingCharacters(
          in: .whitespaces) != parameter.type.description.trimmingCharacters(in: .whitespacesAndNewlines)
      { return false }
    }
    return true
  }

  // Evaluates if all, and only, the stored properties are initialized in the body
  private func matchesAssignmentBody(
    variables: [VariableDeclSyntax],
    initBody: CodeBlockSyntax?
  ) -> Bool {
    guard let initBody = initBody else { return false }
    guard variables.count == initBody.statements.count else { return false }

    var statements: [String] = []
    for statement in initBody.statements {
      guard
        let expr = statement.item.as(InfixOperatorExprSyntax.self),
        expr.operator.is(AssignmentExprSyntax.self)
      else {
        return false
      }

      var leftName = ""
      var rightName = ""

      if let memberAccessExpr = expr.leftOperand.as(MemberAccessExprSyntax.self) {
        guard
          let base = memberAccessExpr.base,
          base.description.trimmingCharacters(in: .whitespacesAndNewlines) == "self"
        else {
          return false
        }

        leftName = memberAccessExpr.declName.baseName.text
      } else {
        return false
      }

      if let identifierExpr = expr.rightOperand.as(DeclReferenceExprSyntax.self) {
        rightName = identifierExpr.baseName.text
      } else {
        return false
      }

      guard leftName == rightName else { return false }
      statements.append(leftName)
    }

    for variable in variables {
      let id = variable.firstIdentifier.identifier.text
      guard statements.contains(id) else { return false }
      guard let idx = statements.firstIndex(of: id) else { return false }
      statements.remove(at: idx)
    }
    return statements.isEmpty
  }
}

extension Finding.Message {
  fileprivate static let removeRedundantInitializer: Finding.Message =
    "remove this explicit initializer, which is identical to the compiler-synthesized initializer"
}

/// Defines the access levels which may be assigned to a synthesized memberwise initializer.
fileprivate enum AccessLevel {
  case `internal`
  case `fileprivate`
  case `private`
}

/// Computes the access level which would be applied to the synthesized memberwise initializer of
/// a struct that contains the given properties.
///
/// The rules for default memberwise initializer access levels are defined in The Swift
/// Programming Language:
/// https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html#ID21
///
/// - Parameter properties: The properties contained within the struct.
/// - Returns: The synthesized memberwise initializer's access level.
fileprivate func synthesizedInitAccessLevel(using properties: [VariableDeclSyntax]) -> AccessLevel {
  var hasFileprivate = false
  for property in properties {
    // Private takes precedence, so finding 1 private property defines the access level.
    if property.modifiers.contains(where: {$0.name.tokenKind == .keyword(.private) && $0.detail == nil}) {
      return .private
    }
    if property.modifiers.contains(where: {$0.name.tokenKind == .keyword(.fileprivate) && $0.detail == nil}) {
      hasFileprivate = true
      // Can't break here because a later property might be private.
    }
  }
  return hasFileprivate ? .fileprivate : .internal
}

// FIXME: Stop using these extensions; they make assumptions about the structure of stored
// properties and may miss some valid cases, like tuple patterns.
extension VariableDeclSyntax {
  /// Returns array of all identifiers listed in the declaration.
  fileprivate var identifiers: [IdentifierPatternSyntax] {
    var ids: [IdentifierPatternSyntax] = []
    for binding in bindings {
      guard let id = binding.pattern.as(IdentifierPatternSyntax.self) else { continue }
      ids.append(id)
    }
    return ids
  }

  /// Returns the first identifier.
  fileprivate var firstIdentifier: IdentifierPatternSyntax {
    return identifiers[0]
  }

  /// Returns the first type explicitly stated in the declaration, if present.
  fileprivate var firstType: TypeSyntax? {
    return bindings.first?.typeAnnotation?.type
  }

  /// Returns the first initializer clause, if present.
  fileprivate var firstInitializer: InitializerClauseSyntax? {
    return bindings.first?.initializer
  }
}