Skip to content

Commit 6ef55c9

Browse files
authored
Fix incorrect reference removal in AssignOnlyPropertyReferenceEliminator when properties have identical names at different scopes. Closes #394 (#433)
1 parent 745f6a1 commit 6ef55c9

File tree

6 files changed

+97
-63
lines changed

6 files changed

+97
-63
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
- Unused function parameters on unimplemented protocol function members are now retained, as the function may still be referenced from an existential type.
1616
- Fix incorrect redundant public accessibility on a public superclass when a subclass is used in another module.
1717
- Result Builder static methods are now retained.
18+
- Assign-only properties that are assigned multiple times in the same method are now correctly identified as assign-only.
19+
- Fix issue where properties with identical names at different scopes could cause inconsistent results from assign-only property analysis.
1820

1921
## 2.8.2 (2021-11-06)
2022

Sources/PeripheryKit/Analyzer/Visitors/AssignOnlyPropertyReferenceEliminator.swift

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,40 @@ final class AssignOnlyPropertyReferenceEliminator: SourceGraphVisitor {
1818
guard !configuration.retainAssignOnlyProperties else { return }
1919

2020
let setters = graph.declarations(ofKind: .functionAccessorSetter)
21-
2221
var assignOnlyProperties: [Declaration: [Reference]] = [:]
2322

2423
for setter in setters {
2524
let references = graph.references(to: setter)
2625

2726
for reference in references {
28-
guard let caller = reference.parent,
29-
let propertyName = setter.name?.replacingOccurrences(of: "setter:", with: ""),
30-
let propertyReference = caller.references.first(where: { $0.kind.isVariableKind && $0.name == propertyName })
31-
else { continue }
32-
33-
guard let property = graph.explicitDeclaration(withUsr: propertyReference.usr),
34-
!property.isComplexProperty
27+
// Ensure the property being assigned is simple.
28+
guard
29+
let caller = reference.parent,
30+
let property = setter.parent,
31+
!property.isComplexProperty
3532
else { continue }
3633

3734
// A protocol property can technically be assigned and never used when the protocol is used as an existential
3835
// type, however communicating that succinctly would be very tricky, and most likely just lead to confusion.
3936
// Here we filter out protocol properties and thus restrict this analysis only to concrete properties.
40-
if property.parent?.kind == .protocol {
41-
continue
37+
guard property.parent?.kind != .protocol else { continue }
38+
39+
// Find all references to the property at the same call site as the setter reference.
40+
let propertyReferences = caller.references.filter {
41+
property.usrs.contains($0.usr) && $0.location == reference.location
4242
}
4343

44-
let propertyGetterUSRs = property.declarations.first { $0.kind == .functionAccessorGetter }?.usrs ?? []
44+
// Determine if the containing method contains references to the property's getter accessor.
45+
let propertyGetterUSRs = property.declarations
46+
.filter { $0.kind == .functionAccessorGetter }
47+
.flatMap { $0.usrs }
4548
let hasGetterReference = caller.references
4649
.contains { $0.kind == .functionAccessorGetter && propertyGetterUSRs.contains($0.usr) }
4750

4851
if !hasGetterReference {
49-
assignOnlyProperties[property, default: []].append(propertyReference)
52+
propertyReferences.forEach {
53+
assignOnlyProperties[property, default: []].append($0)
54+
}
5055
}
5156
}
5257
}

Sources/PeripheryKit/Indexer/Reference.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ public final class Reference {
8686
rawValue.hasPrefix("function")
8787
}
8888

89-
var isVariableKind: Bool {
90-
rawValue.hasPrefix("var")
91-
}
92-
9389
var declarationEquivalent: Declaration.Kind? {
9490
Declaration.Kind(rawValue: rawValue)
9591
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
public class FixtureClass131 {
4+
static var someProperty: String?
5+
var someProperty: String
6+
7+
public init() {
8+
someProperty = ""
9+
Self.someProperty = ""
10+
}
11+
12+
public func someMethod() {
13+
_ = Self.someProperty
14+
}
15+
}

Tests/Fixtures/RetentionFixtures/testSimplePropertyAssignedButNeverRead.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public class FixtureClass70 {
44
static var simpleStaticUnreadVar: String!
55
var simpleUnreadVar: String
66
var simpleUnreadShadowedVar: String
7+
var simpleUnreadVarAssignedMultiple: String
78
var complexUnreadVar1: String {
89
willSet {
910
print("complex")
@@ -25,6 +26,8 @@ public class FixtureClass70 {
2526
init() {
2627
simpleUnreadVar = "Hello"
2728
simpleUnreadShadowedVar = "Hello"
29+
simpleUnreadVarAssignedMultiple = "1"
30+
simpleUnreadVarAssignedMultiple = "2"
2831
complexUnreadVar1 = "Hello"
2932
readVar = "Hello"
3033
FixtureClass70.simpleStaticUnreadVar = "Hello"

Tests/PeripheryTests/RetentionTest.swift

Lines changed: 60 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -745,27 +745,6 @@ final class RetentionTest: SourceGraphTestCase {
745745
}
746746
}
747747

748-
func testRetainsAssignOnlyPropertyTypes() {
749-
configuration.retainAssignOnlyProperties = false
750-
configuration.retainAssignOnlyPropertyTypes = ["CustomType", "(CustomType, String)", "Swift.Double"]
751-
752-
analyze(retainPublic: true) {
753-
assertReferenced(.class("FixtureClass123")) {
754-
self.assertReferenced(.varInstance("retainedSimpleProperty"))
755-
self.assertReferenced(.varInstance("retainedModulePrefixedProperty"))
756-
self.assertReferenced(.varInstance("retainedTupleProperty"))
757-
self.assertReferenced(.varInstance("retainedDestructuredPropertyA"))
758-
self.assertReferenced(.varInstance("retainedMultipleBindingPropertyA"))
759-
760-
self.assertNotReferenced(.varInstance("notRetainedSimpleProperty"))
761-
self.assertNotReferenced(.varInstance("notRetainedModulePrefixedProperty"))
762-
self.assertNotReferenced(.varInstance("notRetainedTupleProperty"))
763-
self.assertNotReferenced(.varInstance("notRetainedDestructuredPropertyB"))
764-
self.assertNotReferenced(.varInstance("notRetainedMultipleBindingPropertyB"))
765-
}
766-
}
767-
}
768-
769748
func testNestedDeclarations() {
770749
analyze(retainPublic: true) {
771750
assertReferenced(.class("FixtureClass102")) {
@@ -903,32 +882,6 @@ final class RetentionTest: SourceGraphTestCase {
903882
}
904883
}
905884

906-
func testSimplePropertyAssignedButNeverRead() {
907-
analyze(retainPublic: true) {
908-
assertReferenced(.class("FixtureClass70")) {
909-
self.assertNotReferenced(.varInstance("simpleUnreadVar"))
910-
self.assertNotReferenced(.varInstance("simpleUnreadShadowedVar"))
911-
self.assertNotReferenced(.varStatic("simpleStaticUnreadVar"))
912-
self.assertReferenced(.varInstance("complexUnreadVar1"))
913-
self.assertReferenced(.varInstance("complexUnreadVar2"))
914-
self.assertReferenced(.varInstance("readVar"))
915-
}
916-
}
917-
918-
configuration.retainAssignOnlyProperties = true
919-
920-
analyze(retainPublic: true) {
921-
assertReferenced(.class("FixtureClass70")) {
922-
self.assertReferenced(.varInstance("simpleUnreadVar"))
923-
self.assertReferenced(.varInstance("simpleUnreadShadowedVar"))
924-
self.assertReferenced(.varStatic("simpleStaticUnreadVar"))
925-
self.assertReferenced(.varInstance("complexUnreadVar1"))
926-
self.assertReferenced(.varInstance("complexUnreadVar2"))
927-
self.assertReferenced(.varInstance("readVar"))
928-
}
929-
}
930-
}
931-
932885
func testRetainsProtocolsViaCompositeTypealias() {
933886
analyze(retainPublic: true) {
934887
assertReferenced(.protocol("Fixture200"))
@@ -1013,6 +966,66 @@ final class RetentionTest: SourceGraphTestCase {
1013966
}
1014967
#endif
1015968

969+
// MARK: - Assign-only properties
970+
971+
func testSimplePropertyAssignedButNeverRead() {
972+
analyze(retainPublic: true) {
973+
assertReferenced(.class("FixtureClass70")) {
974+
self.assertNotReferenced(.varInstance("simpleUnreadVar"))
975+
self.assertNotReferenced(.varInstance("simpleUnreadShadowedVar"))
976+
self.assertNotReferenced(.varInstance("simpleUnreadVarAssignedMultiple"))
977+
self.assertNotReferenced(.varStatic("simpleStaticUnreadVar"))
978+
self.assertReferenced(.varInstance("complexUnreadVar1"))
979+
self.assertReferenced(.varInstance("complexUnreadVar2"))
980+
self.assertReferenced(.varInstance("readVar"))
981+
}
982+
}
983+
984+
configuration.retainAssignOnlyProperties = true
985+
986+
analyze(retainPublic: true) {
987+
assertReferenced(.class("FixtureClass70")) {
988+
self.assertReferenced(.varInstance("simpleUnreadVar"))
989+
self.assertReferenced(.varInstance("simpleUnreadShadowedVar"))
990+
self.assertReferenced(.varInstance("simpleUnreadVarAssignedMultiple"))
991+
self.assertReferenced(.varStatic("simpleStaticUnreadVar"))
992+
self.assertReferenced(.varInstance("complexUnreadVar1"))
993+
self.assertReferenced(.varInstance("complexUnreadVar2"))
994+
self.assertReferenced(.varInstance("readVar"))
995+
}
996+
}
997+
}
998+
999+
func testSimpleAssignOnlyPropertyNameConflict() {
1000+
analyze(retainPublic: true) {
1001+
assertReferenced(.class("FixtureClass131")) {
1002+
self.assertNotReferenced(.varInstance("someProperty"))
1003+
self.assertReferenced(.varStatic("someProperty"))
1004+
}
1005+
}
1006+
}
1007+
1008+
func testRetainsAssignOnlyPropertyTypes() {
1009+
configuration.retainAssignOnlyProperties = false
1010+
configuration.retainAssignOnlyPropertyTypes = ["CustomType", "(CustomType, String)", "Swift.Double"]
1011+
1012+
analyze(retainPublic: true) {
1013+
assertReferenced(.class("FixtureClass123")) {
1014+
self.assertReferenced(.varInstance("retainedSimpleProperty"))
1015+
self.assertReferenced(.varInstance("retainedModulePrefixedProperty"))
1016+
self.assertReferenced(.varInstance("retainedTupleProperty"))
1017+
self.assertReferenced(.varInstance("retainedDestructuredPropertyA"))
1018+
self.assertReferenced(.varInstance("retainedMultipleBindingPropertyA"))
1019+
1020+
self.assertNotReferenced(.varInstance("notRetainedSimpleProperty"))
1021+
self.assertNotReferenced(.varInstance("notRetainedModulePrefixedProperty"))
1022+
self.assertNotReferenced(.varInstance("notRetainedTupleProperty"))
1023+
self.assertNotReferenced(.varInstance("notRetainedDestructuredPropertyB"))
1024+
self.assertNotReferenced(.varInstance("notRetainedMultipleBindingPropertyB"))
1025+
}
1026+
}
1027+
}
1028+
10161029
// MARK: - Unused Parameters
10171030

10181031
func testRetainsParamUsedInOverriddenMethod() {

0 commit comments

Comments
 (0)