From c717367a474d2dd384162314433f283ccfa6870c Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:02:26 -0700 Subject: [PATCH 1/2] Add failing repro: sole parameterized class init flagged unused A class retained only as a property type, whose single explicit initializer takes parameters and has no external callers, has that initializer reported as unused. Removing it would leave the class's non-default stored property un-initializable. DefaultConstructorReferenceBuilder only retains init() and implicit initializers, so a parameterized sole designated initializer is not covered. This test pins the behavior for inspection. --- ...testRetainsSoleRequiredClassInitializer.swift | 16 ++++++++++++++++ Tests/PeripheryTests/RetentionTest.swift | 8 ++++++++ 2 files changed, 24 insertions(+) create mode 100644 Tests/Fixtures/Sources/RetentionFixtures/testRetainsSoleRequiredClassInitializer.swift diff --git a/Tests/Fixtures/Sources/RetentionFixtures/testRetainsSoleRequiredClassInitializer.swift b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsSoleRequiredClassInitializer.swift new file mode 100644 index 000000000..0aea17131 --- /dev/null +++ b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsSoleRequiredClassInitializer.swift @@ -0,0 +1,16 @@ +import Foundation + +// A class with a stored property that has no default value, and a single explicit +// initializer that assigns it. The class itself is retained (referenced as the type +// of a retained property below), but its sole init has no external callers. +public class FixtureClass400 { + private var value: Int + + init(value: Int) { + self.value = value + } +} + +public class FixtureClass400Retainer { + public var page: FixtureClass400? +} diff --git a/Tests/PeripheryTests/RetentionTest.swift b/Tests/PeripheryTests/RetentionTest.swift index 599bb83ec..9e2d05ae1 100644 --- a/Tests/PeripheryTests/RetentionTest.swift +++ b/Tests/PeripheryTests/RetentionTest.swift @@ -1095,6 +1095,14 @@ final class RetentionTest: FixtureSourceGraphTestCase { } } + func testRetainsSoleRequiredClassInitializer() { + analyze(retainPublic: true) { + assertReferenced(.class("FixtureClass400")) { + self.assertReferenced(.functionConstructor("init(value:)")) + } + } + } + // https://github.com/apple/swift/issues/56541 func testStaticMemberUsedAsSubscriptKey() { analyze(retainPublic: true) { From 8fc9580cc47dfb0fe524a6ff1266a87d0c147b45 Mon Sep 17 00:00:00 2001 From: Dan Wood <207080+danwood@users.noreply.github.com> Date: Fri, 12 Jun 2026 22:15:43 -0700 Subject: [PATCH 2/2] Retain a class's sole designated initializer when it has stored state A struct synthesizes a memberwise initializer, so an explicit struct init that's never called is genuinely dead code. A class has no such synthesis: when a class declares a stored property without a default value, the explicit initializer is the only way to construct an instance, and removing it would make the class unconstructable. DefaultConstructorReferenceBuilder previously only retained init() and implicit initializers, so a class's sole parameterized designated initializer with no external callers was reported as unused. Retain that initializer, but narrowly. It fires only when the parent is a class with exactly one non-implicit initializer and at least one stored instance property. If a class declares multiple explicit initializers there are alternatives, so none are retained. Structs and enums are excluded because they synthesize their own initializers. This keeps the change from masking the dead init cases discussed in issue #1058. Also adds scope tests: multiple class inits stay reportable, a sole struct init stays reportable, and a used sole class init remains referenced. --- .../DefaultConstructorReferenceBuilder.swift | 67 +++++++++++++++---- ...lassInitializersWhenMultipleDeclared.swift | 16 +++++ ...stDoesNotRetainSoleStructInitializer.swift | 12 ++++ .../testRetainsUsedSoleClassInitializer.swift | 18 +++++ Tests/PeripheryTests/RetentionTest.swift | 25 +++++++ 5 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainClassInitializersWhenMultipleDeclared.swift create mode 100644 Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainSoleStructInitializer.swift create mode 100644 Tests/Fixtures/Sources/RetentionFixtures/testRetainsUsedSoleClassInitializer.swift diff --git a/Sources/SourceGraph/Mutators/DefaultConstructorReferenceBuilder.swift b/Sources/SourceGraph/Mutators/DefaultConstructorReferenceBuilder.swift index 743c2bbc4..6f0d5814a 100644 --- a/Sources/SourceGraph/Mutators/DefaultConstructorReferenceBuilder.swift +++ b/Sources/SourceGraph/Mutators/DefaultConstructorReferenceBuilder.swift @@ -11,6 +11,7 @@ final class DefaultConstructorReferenceBuilder: SourceGraphMutator { func mutate() { referenceDefaultConstructors() + referenceSoleClassInitializers() referenceDestructors() } @@ -24,19 +25,61 @@ final class DefaultConstructorReferenceBuilder: SourceGraphMutator { } for constructor in defaultConstructors { - if let parent = constructor.parent { - for usr in constructor.usrs { - let reference = Reference( - name: constructor.name, - kind: .normal, - declarationKind: .functionConstructor, - usr: usr, - location: parent.location - ) - reference.parent = parent - graph.add(reference, from: parent) - } + reference(constructor) + } + } + + /// Retains a class's sole designated initializer when the class has stored state to initialize. + /// + /// A struct synthesizes a memberwise initializer, so an explicit struct init that's never called + /// is genuinely dead code. A class has no such synthesis: when a class declares a stored property + /// without a default value, the explicit initializer is the only way to construct an instance, and + /// removing it would make the class unconstructable. This narrow case retains that initializer. + /// + /// The retention is deliberately limited to avoid masking dead code (see issue #1058): + /// it fires only when the class has exactly one non-implicit initializer and at least one stored + /// instance property. If the class has multiple explicit initializers there are alternatives, so + /// none are retained here. If the class itself is unused it folds away entirely along with its + /// initializer, so this retention cannot keep a dead class alive. + private func referenceSoleClassInitializers() { + for classDecl in graph.declarations(ofKind: .class) { + let explicitInits = classDecl.declarations.filter { + $0.kind == .functionConstructor && !$0.isImplicit } + + guard explicitInits.count == 1, let soleInit = explicitInits.first else { continue } + + let hasStoredProperty = classDecl.declarations.contains { decl in + decl.kind == .varInstance && !isComputedProperty(decl) + } + + guard hasStoredProperty else { continue } + + reference(soleInit) + } + } + + /// A computed property is identified by a getter accessor with no corresponding setter accessor. + /// A stored property either declares no accessors or declares a synthesized get/set pair. + private func isComputedProperty(_ property: Declaration) -> Bool { + let hasGetter = property.declarations.contains { $0.kind == .functionAccessorGetter } + let hasSetter = property.declarations.contains { $0.kind == .functionAccessorSetter } + return hasGetter && !hasSetter + } + + private func reference(_ constructor: Declaration) { + guard let parent = constructor.parent else { return } + + for usr in constructor.usrs { + let reference = Reference( + name: constructor.name, + kind: .normal, + declarationKind: .functionConstructor, + usr: usr, + location: parent.location + ) + reference.parent = parent + graph.add(reference, from: parent) } } diff --git a/Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainClassInitializersWhenMultipleDeclared.swift b/Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainClassInitializersWhenMultipleDeclared.swift new file mode 100644 index 000000000..54ca72394 --- /dev/null +++ b/Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainClassInitializersWhenMultipleDeclared.swift @@ -0,0 +1,16 @@ +import Foundation + +// A class with stored state but TWO explicit initializers. Neither has external callers. +// The narrow sole-init retention must NOT fire here: the user has alternatives, so both +// initializers should remain reportable as unused. +public class FixtureClass401 { + private var value: Int + + init(value: Int) { + self.value = value + } + + init(other: Int) { + value = other + } +} diff --git a/Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainSoleStructInitializer.swift b/Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainSoleStructInitializer.swift new file mode 100644 index 000000000..37338bc5c --- /dev/null +++ b/Tests/Fixtures/Sources/RetentionFixtures/testDoesNotRetainSoleStructInitializer.swift @@ -0,0 +1,12 @@ +import Foundation + +// A struct with a sole explicit initializer that has no external callers. A struct synthesizes +// a memberwise initializer, so an unused explicit struct init is genuinely dead code. The +// class-specific retention must NOT extend to structs: this init should remain reportable as unused. +public struct FixtureStruct225 { + private var value: Int + + init(value: Int) { + self.value = value + } +} diff --git a/Tests/Fixtures/Sources/RetentionFixtures/testRetainsUsedSoleClassInitializer.swift b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsUsedSoleClassInitializer.swift new file mode 100644 index 000000000..f8d6d3d64 --- /dev/null +++ b/Tests/Fixtures/Sources/RetentionFixtures/testRetainsUsedSoleClassInitializer.swift @@ -0,0 +1,18 @@ +import Foundation + +// A class with stored state and a sole explicit initializer that IS called. This is a sanity +// check: the initializer is referenced by its caller regardless of the sole-init retention, and +// remains retained. +public class FixtureClass402 { + private var value: Int + + init(value: Int) { + self.value = value + } +} + +public class FixtureClass402Retainer { + public func build() -> FixtureClass402 { + FixtureClass402(value: 1) + } +} diff --git a/Tests/PeripheryTests/RetentionTest.swift b/Tests/PeripheryTests/RetentionTest.swift index 9e2d05ae1..577e98840 100644 --- a/Tests/PeripheryTests/RetentionTest.swift +++ b/Tests/PeripheryTests/RetentionTest.swift @@ -1103,6 +1103,31 @@ final class RetentionTest: FixtureSourceGraphTestCase { } } + func testDoesNotRetainClassInitializersWhenMultipleDeclared() { + analyze(retainPublic: true) { + assertReferenced(.class("FixtureClass401")) { + self.assertNotReferenced(.functionConstructor("init(value:)")) + self.assertNotReferenced(.functionConstructor("init(other:)")) + } + } + } + + func testDoesNotRetainSoleStructInitializer() { + analyze(retainPublic: true) { + assertReferenced(.struct("FixtureStruct225")) { + self.assertNotReferenced(.functionConstructor("init(value:)")) + } + } + } + + func testRetainsUsedSoleClassInitializer() { + analyze(retainPublic: true) { + assertReferenced(.class("FixtureClass402")) { + self.assertReferenced(.functionConstructor("init(value:)")) + } + } + } + // https://github.com/apple/swift/issues/56541 func testStaticMemberUsedAsSubscriptKey() { analyze(retainPublic: true) {