Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRule.swift
T
Danny Mösch 58928b7e40 Enforce any on existential types (#5273)
This makes syntactically clear which types are rather expensive.
2023-10-12 08:37:23 +02:00

301 lines
12 KiB
Swift

// swiftlint:disable file_header
//
// Adapted from swift-format's UseSynthesizedInitializer.swift
//
// https://github.com/apple/swift-format
//
// 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
@SwiftSyntaxRule
struct UnneededSynthesizedInitializerRule: SwiftSyntaxCorrectableRule, ConfigurationProviderRule {
var configuration = SeverityConfiguration<Self>(.warning)
static let description = RuleDescription(
identifier: "unneeded_synthesized_initializer",
name: "Unneeded Synthesized Initializer",
description: "Default or memberwise initializers that will be automatically synthesized " +
"do not need to be manually defined.",
kind: .idiomatic,
nonTriggeringExamples: UnneededSynthesizedInitializerRuleExamples.nonTriggering,
triggeringExamples: UnneededSynthesizedInitializerRuleExamples.triggering,
corrections: UnneededSynthesizedInitializerRuleExamples.corrections
)
func makeRewriter(file: SwiftLintFile) -> (some ViolationsSyntaxRewriter)? {
Rewriter(
locationConverter: file.locationConverter,
disabledRegions: disabledRegions(file: file)
)
}
}
private extension UnneededSynthesizedInitializerRule {
final class Visitor: ViolationsSyntaxVisitor {
override var skippableDeclarations: [any DeclSyntaxProtocol.Type] {
.allExcept(StructDeclSyntax.self, ClassDeclSyntax.self)
}
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
violations += node.unneededInitializers.map {
let initializerType = $0.parameterList.isEmpty ? "default" : "memberwise"
let reason = "This \(initializerType) initializer would be synthesized automatically - " +
"you do not need to define it"
return ReasonedRuleViolation(position: $0.positionAfterSkippingLeadingTrivia, reason: reason)
}
return .visitChildren
}
}
final class Rewriter: SyntaxRewriter, ViolationsSyntaxRewriter {
var correctionPositions: [AbsolutePosition] = []
private var unneededInitializers: [InitializerDeclSyntax] = []
let locationConverter: SourceLocationConverter
let disabledRegions: [SourceRange]
init(locationConverter: SourceLocationConverter, disabledRegions: [SourceRange]) {
self.locationConverter = locationConverter
self.disabledRegions = disabledRegions
}
override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
unneededInitializers = node.unneededInitializers.filter {
!$0.isContainedIn(regions: disabledRegions, locationConverter: locationConverter)
}
return super.visit(node)
}
override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax {
if unneededInitializers.contains(node) {
correctionPositions.append(node.positionAfterSkippingLeadingTrivia)
let expr: DeclSyntax = ""
return expr
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
}
return super.visit(node)
}
}
}
private extension StructDeclSyntax {
var unneededInitializers: [InitializerDeclSyntax] {
let unneededInitializers = findUnneededInitializers()
let initializersCount = memberBlock.members.filter { $0.decl.is(InitializerDeclSyntax.self) }.count
if unneededInitializers.count == initializersCount {
return unneededInitializers
}
return []
}
// Collects all of the initializers that could be replaced by the synthesized
// memberwise or default initializer(s).
private func findUnneededInitializers() -> [InitializerDeclSyntax] {
var storedProperties: [VariableDeclSyntax] = []
var initializers: [InitializerDeclSyntax] = []
for memberItem in memberBlock.members {
let member = memberItem.decl
// Collect all stored variables into a list.
if let varDecl = member.as(VariableDeclSyntax.self) {
if !varDecl.modifiers.contains(keyword: .static) {
storedProperties.append(varDecl)
}
} else if let initDecl = member.as(InitializerDeclSyntax.self),
initDecl.optionalMark == nil,
!initDecl.hasThrowsOrRethrowsKeyword {
// Collect any possible redundant initializers into a list.
initializers.append(initDecl)
}
}
return initializers.filter {
self.initializerParameters($0.parameterList, match: storedProperties) &&
(($0.parameterList.isEmpty && hasNoSideEffects($0.body)) ||
initializerBody($0.body, matches: storedProperties)) &&
initializerModifiers($0.modifiers, match: storedProperties) && !$0.isInlinable
}
}
// Are the initializer parameters empty, or do they match the stored properties of the struct?
private func initializerParameters(
_ initializerParameters: FunctionParameterListSyntax,
match storedProperties: [VariableDeclSyntax]
) -> Bool {
if initializerParameters.isEmpty {
// Are all properties initialized?
return storedProperties.allSatisfy {
$0.bindingSpecifier.tokenKind == .keyword(.var) && $0.bindings.first?.initializer != nil
}
}
guard initializerParameters.count == storedProperties.count else {
return false
}
for (idx, parameter) in initializerParameters.enumerated() {
guard parameter.secondName == nil else {
return false
}
let property = storedProperties[idx]
let propertyId = property.firstIdentifier
let propertyTypeDescription = property.typeDescription
// 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.
if property.bindingSpecifier.tokenKind == .keyword(.var), let initializer = property.initializer {
guard initializer.value.description == parameter.defaultValue?.value.description else {
return false
}
} else if parameter.defaultValue != nil ||
propertyId.identifier.text != parameter.firstName.text ||
(propertyTypeDescription != nil && propertyTypeDescription != parameter.typeDescription) {
return false
}
}
return true
}
// Does the body initialize all, and only, the stored properties for the struct?
private func initializerBody( // swiftlint:disable:this cyclomatic_complexity
_ initializerBody: CodeBlockSyntax?,
matches storedProperties: [VariableDeclSyntax]
) -> Bool {
guard let initializerBody, storedProperties.count == initializerBody.statements.count else {
return false
}
var statements: [String] = []
for statement in initializerBody.statements {
guard let exp = statement.item.as(SequenceExprSyntax.self) else {
return false
}
var leftName = ""
var rightName = ""
for element in exp.elements {
switch Syntax(element).as(SyntaxEnum.self) {
case .memberAccessExpr(let element):
guard element.isBaseSelf else {
return false
}
leftName = element.declName.baseName.text
case .assignmentExpr(let element) where element.equal.tokenKind != .equal:
return false
case .assignmentExpr:
break
case .declReferenceExpr(let element):
rightName = element.baseName.text
default:
return false
}
}
guard leftName == rightName else {
return false
}
statements.append(leftName)
}
for variable in storedProperties {
let id = variable.firstIdentifier.identifier.text
guard statements.contains(id), let idx = statements.firstIndex(of: id) else {
return false
}
statements.remove(at: idx)
}
return statements.isEmpty
}
private func hasNoSideEffects(_ initializerBody: CodeBlockSyntax?) -> Bool {
guard let initializerBody else {
return true
}
return initializerBody.statements.isEmpty
}
// Does the actual access level of an initializer match the access level of the synthesized
// memberwise initializer?
private func initializerModifiers(
_ modifiers: DeclModifierListSyntax?,
match storedProperties: [VariableDeclSyntax]
) -> Bool {
let accessLevel = modifiers?.accessLevelModifier
switch synthesizedInitializerAccessLevel(using: storedProperties) {
case .internal:
// No explicit access level or internal are equivalent.
return accessLevel == nil || accessLevel!.name.tokenKind == .keyword(.internal)
case .fileprivate:
return accessLevel?.name.tokenKind == .keyword(.fileprivate)
case .private:
return accessLevel?.name.tokenKind == .keyword(.private)
}
}
}
private extension InitializerDeclSyntax {
var hasThrowsOrRethrowsKeyword: Bool {
signature.effectSpecifiers?.throwsSpecifier != nil
}
var isInlinable: Bool {
attributes.contains(attributeNamed: "inlinable")
}
var parameterList: FunctionParameterListSyntax {
signature.parameterClause.parameters
}
}
private extension FunctionParameterSyntax {
var typeDescription: String {
type.description.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
private extension VariableDeclSyntax {
var identifiers: [IdentifierPatternSyntax] {
bindings.compactMap { $0.pattern.as(IdentifierPatternSyntax.self) }
}
var firstIdentifier: IdentifierPatternSyntax {
identifiers[0]
}
var typeDescription: String? {
bindings.first?.typeAnnotation?.type.description.trimmingCharacters(in: .whitespaces)
}
var initializer: InitializerClauseSyntax? {
bindings.first?.initializer
}
}
// Defines the access levels which may be assigned to a synthesized memberwise initializer.
private enum AccessLevel {
case `internal`
case `fileprivate`
case `private`
}
// See https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html#ID21
// for the rules defining default memberwise initializer access levels
private func synthesizedInitializerAccessLevel(using storedProperties: [VariableDeclSyntax]) -> AccessLevel {
var hasFileprivate = false
for property in storedProperties {
let modifiers = property.modifiers
// Private takes precedence, so finding 1 private property defines the access level.
if modifiers.contains(where: { $0.name.tokenKind == .keyword(.private) && $0.detail == nil }) {
return .private
}
if 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
}