Files
SwiftLint/Source/SwiftLintBuiltInRules/Rules/Idiomatic/UnneededSynthesizedInitializerRule.swift
Danny Mösch 47335d7f95 Remove tracking of correction positions (#5950)
Report number of corrections per file instead.
2025-04-05 06:04:37 -04:00

291 lines
11 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(explicitRewriter: true)
struct UnneededSynthesizedInitializerRule: Rule {
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
)
}
private extension UnneededSynthesizedInitializerRule {
final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
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: ViolationsSyntaxRewriter<ConfigurationType> {
private var unneededInitializers: [InitializerDeclSyntax] = []
override func visitAny(_: Syntax) -> Syntax? { nil }
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) {
numberOfCorrections += 1
let expr: DeclSyntax = ""
return expr
.with(\.leadingTrivia, node.leadingTrivia)
.with(\.trailingTrivia, node.trailingTrivia)
}
return super.visit(node)
}
}
}
private final class ElementCollector: SyntaxAnyVisitor {
var initializers = [InitializerDeclSyntax]()
var varDecls = [VariableDeclSyntax]()
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
node.isProtocol((any NamedDeclSyntax).self) ? .skipChildren : .visitChildren
}
override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
initializers.append(node)
return .skipChildren
}
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
varDecls.append(node)
return .skipChildren
}
}
private extension StructDeclSyntax {
var unneededInitializers: [InitializerDeclSyntax] {
let collector = ElementCollector(viewMode: .sourceAccurate)
collector.walk(memberBlock)
let unneededInitializers = findUnneededInitializers(in: collector)
if unneededInitializers.count == collector.initializers.count {
return unneededInitializers
}
return []
}
// Finds all of the initializers that could be replaced by the synthesized
// memberwise or default initializer(s).
private func findUnneededInitializers(in collector: ElementCollector) -> [InitializerDeclSyntax] {
let initializers = collector.initializers.filter {
$0.optionalMark == nil && !$0.hasThrowsOrRethrowsKeyword
}
let varDecls = collector.varDecls.filter { !$0.modifiers.contains(keyword: .static) }
return initializers.filter {
self.initializerParameters($0.parameterList, match: varDecls) &&
(($0.parameterList.isEmpty && hasNoSideEffects($0.body)) ||
initializerBody($0.body, matches: varDecls)) &&
initializerModifiers($0.modifiers, match: varDecls) && $0.attributes.isEmpty
}
}
// 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, parameter.attributes.isEmpty 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?.throwsClause?.throwsSpecifier != nil
}
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
}