Skip to content

Don't report unused initializer if removing it would break the build #1058

@danwood

Description

@danwood

Describe the bug

Periphery reports an initializer as unused when it is the sole initializer for a class/struct that has stored properties without default values. Removing such an initializer would cause a compilation error, making this warning actionable only by removing the entire type.

The warning is technically correct (the init is never called), but following the suggestion to remove it produces non-compiling code.

Reproduction

fileprivate class TempoMap {
    private let tempoEvents: [TempoEvent]
    private let fallbackTempo: Double

    init(tempoEvents: [TempoEvent], fallbackTempo: Double) {  // ⚠️ reported as unused
        self.tempoEvents = tempoEvents.sorted { $0.positionBars < $1.positionBars }
        self.fallbackTempo = fallbackTempo
    }

    func tempo(atTime: TimeInterval) -> Double {
        return fallbackTempo
    }

    func tempo(atBars bars: Int) -> Double {
        let applicableEvents = tempoEvents.filter { $0.positionBars <= bars }
        return applicableEvents.last?.tempo ?? fallbackTempo
    }
}

struct TempoEvent: Codable, Equatable {
    let positionBars: Int
    let positionBeats: Int
    let tempo: Double
}

Running Periphery produces:

warning: Initializer 'init(tempoEvents:fallbackTempo:)' is unused

If you remove the initializer as suggested, the code fails to compile:

error: class 'TempoMap' has no initializers
note: stored property 'tempoEvents' without initial value prevents synthesized initializers
note: stored property 'fallbackTempo' without initial value prevents synthesized initializers

Expected behavior

When an initializer is the only initializer for a type that requires one (due to stored properties without defaults), it should not be reported as unused. Instead, either:

  1. Don't report the initializer (it's required for the type to compile)
  2. Report the entire type as unused (if the type itself is never instantiated)

Potential fix location

DefaultConstructorReferenceBuilder.swift currently creates synthetic references only for init() (no parameters) and implicit initializers. It could be extended to also reference initializers that are the sole initializer for their parent type:

// In referenceDefaultConstructors()
let constructors = graph.declarations(ofKind: .functionConstructor)
let constructorsByParent = Dictionary(grouping: constructors) { $0.parent }

for (parent, parentConstructors) in constructorsByParent {
    guard let parent = parent else { continue }

    for constructor in parentConstructors {
        let isDefault = constructor.name == "init()" || constructor.isImplicit
        let isSoleConstructor = parentConstructors.count == 1

        if isDefault || isSoleConstructor {
            // Create synthetic reference from parent to constructor
        }
    }
}

Environment

3.4.0
swift-driver version: 1.127.14.1 Apple Swift version 6.2.3 (swiftlang-6.2.3.3.21 clang-1700.6.3.2)
Target: arm64-apple-macosx26.0
Xcode 26.2
Build version 17C52

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions