mirror of
https://github.com/realm/SwiftLint.git
synced 2026-05-07 20:12:49 +00:00
58928b7e40
This makes syntactically clear which types are rather expensive.
301 lines
12 KiB
Swift
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
|
|
}
|