Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ final class DefaultConstructorReferenceBuilder: SourceGraphMutator {

func mutate() {
referenceDefaultConstructors()
referenceSoleClassInitializers()
referenceDestructors()
}

Expand All @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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?
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
33 changes: 33 additions & 0 deletions Tests/PeripheryTests/RetentionTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,39 @@ final class RetentionTest: FixtureSourceGraphTestCase {
}
}

func testRetainsSoleRequiredClassInitializer() {
analyze(retainPublic: true) {
assertReferenced(.class("FixtureClass400")) {
self.assertReferenced(.functionConstructor("init(value:)"))
}
}
}

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) {
Expand Down
Loading