Skip to content

Commit be40cf8

Browse files
committed
KT-12152 : constructor consistency: distinguish properties with and w/o backing fields, with default / custom accessors
1 parent 422ea4c commit be40cf8

File tree

10 files changed

+147
-17
lines changed

10 files changed

+147
-17
lines changed

compiler/frontend/src/org/jetbrains/kotlin/cfg/ConstructorConsistencyChecker.kt

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.jetbrains.kotlin.cfg
1818

19+
import com.intellij.psi.PsiElement
1920
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
2021
import org.jetbrains.kotlin.cfg.pseudocode.Pseudocode
2122
import org.jetbrains.kotlin.cfg.pseudocode.instructions.KtElementInstruction
@@ -26,6 +27,8 @@ import org.jetbrains.kotlin.cfg.pseudocodeTraverser.TraversalOrder
2627
import org.jetbrains.kotlin.cfg.pseudocodeTraverser.traverse
2728
import org.jetbrains.kotlin.descriptors.*
2829
import org.jetbrains.kotlin.psi.*
30+
import org.jetbrains.kotlin.lexer.KtTokens
31+
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
2932
import org.jetbrains.kotlin.resolve.BindingContext
3033
import org.jetbrains.kotlin.resolve.BindingTrace
3134
import org.jetbrains.kotlin.types.expressions.OperatorConventions
@@ -47,26 +50,38 @@ class ConstructorConsistencyChecker private constructor(
4750
) {
4851
private val finalClass = classDescriptor.isFinalClass
4952

50-
private fun checkOpenPropertyAccess(reference: KtReferenceExpression) {
51-
if (!finalClass) {
52-
val descriptor = trace.get(BindingContext.REFERENCE_TARGET, reference)
53-
if (descriptor is PropertyDescriptor && descriptor.isOverridable) {
53+
private fun insideLValue(reference: KtReferenceExpression): Boolean {
54+
val binary = reference.getStrictParentOfType<KtBinaryExpression>() ?: return false
55+
if (binary.operationToken in KtTokens.ALL_ASSIGNMENTS) {
56+
val binaryLeft = binary.left
57+
var current: PsiElement = reference
58+
while (current !== binaryLeft && current !== binary) {
59+
current = current.parent ?: return false
60+
}
61+
return current === binaryLeft
62+
}
63+
return false
64+
}
65+
66+
private fun safeReferenceUsage(reference: KtReferenceExpression): Boolean {
67+
val descriptor = trace.get(BindingContext.REFERENCE_TARGET, reference)
68+
if (descriptor is PropertyDescriptor) {
69+
if (!finalClass && descriptor.isOverridable) {
5470
trace.record(BindingContext.LEAKING_THIS, reference, LeakingThisDescriptor.NonFinalProperty(descriptor, classOrObject))
71+
return true
5572
}
73+
if (descriptor.containingDeclaration != classDescriptor) return true
74+
if (insideLValue(reference)) return descriptor.setter?.isDefault != false else return descriptor.getter?.isDefault != false
5675
}
76+
return true
5777
}
5878

5979
private fun safeThisUsage(expression: KtThisExpression): Boolean {
6080
val referenceDescriptor = trace.get(BindingContext.REFERENCE_TARGET, expression.instanceReference)
6181
if (referenceDescriptor != classDescriptor) return true
6282
val parent = expression.parent
6383
return when (parent) {
64-
is KtQualifiedExpression ->
65-
if (parent.selectorExpression is KtSimpleNameExpression) {
66-
checkOpenPropertyAccess(parent.selectorExpression as KtSimpleNameExpression)
67-
true
68-
}
69-
else false
84+
is KtQualifiedExpression -> (parent.selectorExpression as? KtSimpleNameExpression)?.let { safeReferenceUsage(it) } ?: false
7085
is KtBinaryExpression -> OperatorConventions.IDENTITY_EQUALS_OPERATIONS.contains(parent.operationToken)
7186
else -> false
7287
}
@@ -132,7 +147,9 @@ class ConstructorConsistencyChecker private constructor(
132147
}
133148
}
134149
else if (element is KtReferenceExpression) {
135-
checkOpenPropertyAccess(element)
150+
if (!safeReferenceUsage(element)) {
151+
handleLeakingThis(element)
152+
}
136153
}
137154
}
138155
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
class My {
2+
var x = 1
3+
set(value) {
4+
field = value
5+
}
6+
7+
var y: Int = 1
8+
set(value) {
9+
field = value + if (w == "") 0 else 1
10+
}
11+
12+
var z: Int = 2
13+
set(value) {
14+
field = value + if (w == "") 1 else 0
15+
}
16+
17+
init {
18+
// Writing properties using setters is dangerous
19+
<!DEBUG_INFO_LEAKING_THIS!>y<!> = 4
20+
this.<!DEBUG_INFO_LEAKING_THIS!>z<!> = 5
21+
}
22+
23+
val w = "6"
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package
2+
3+
public final class My {
4+
public constructor My()
5+
public final val w: kotlin.String = "6"
6+
public final var x: kotlin.Int
7+
public final var y: kotlin.Int
8+
public final var z: kotlin.Int
9+
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
10+
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
11+
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class My(var x: String) {
2+
3+
var y: String
4+
get() = if (x != "") x else z
5+
set(arg) {
6+
if (arg != "") x = arg
7+
}
8+
9+
val z: String
10+
11+
init {
12+
// Dangerous: setter!
13+
<!DEBUG_INFO_LEAKING_THIS!>y<!> = "x"
14+
// Dangerous: getter!
15+
if (<!DEBUG_INFO_LEAKING_THIS!>y<!> != "") z = this.<!DEBUG_INFO_LEAKING_THIS!>y<!> else z = <!DEBUG_INFO_LEAKING_THIS!>y<!>
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package
2+
3+
public final class My {
4+
public constructor My(/*0*/ x: kotlin.String)
5+
public final var x: kotlin.String
6+
public final var y: kotlin.String
7+
public final val z: kotlin.String
8+
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
9+
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
10+
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class My {
2+
val x: Int
3+
get() = field + if (z != "") 1 else 0
4+
5+
val y: Int
6+
get() = field - if (z == "") 0 else 1
7+
8+
val w: Int
9+
10+
init {
11+
// Safe, val never has a setter
12+
x = 0
13+
this.y = 0
14+
// Unsafe
15+
w = this.<!DEBUG_INFO_LEAKING_THIS!>x<!> + <!DEBUG_INFO_LEAKING_THIS!>y<!>
16+
}
17+
18+
val z = "1"
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package
2+
3+
public final class My {
4+
public constructor My()
5+
public final val w: kotlin.Int
6+
public final val x: kotlin.Int
7+
public final val y: kotlin.Int
8+
public final val z: kotlin.String = "1"
9+
public open override /*1*/ /*fake_override*/ fun equals(/*0*/ other: kotlin.Any?): kotlin.Boolean
10+
public open override /*1*/ /*fake_override*/ fun hashCode(): kotlin.Int
11+
public open override /*1*/ /*fake_override*/ fun toString(): kotlin.String
12+
}

compiler/testData/diagnostics/tests/controlFlowAnalysis/delegatedPropertyEarlyAccess.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ class CustomDelegate {
99
class Kaboom() {
1010
// Here and below we should have errors for simple AND delegated
1111
init {
12-
<!UNINITIALIZED_VARIABLE!>delegated<!>.hashCode()
12+
<!UNINITIALIZED_VARIABLE, DEBUG_INFO_LEAKING_THIS!>delegated<!>.hashCode()
1313
<!UNINITIALIZED_VARIABLE!>simple<!>.hashCode()
14-
withGetter.hashCode()
14+
<!DEBUG_INFO_LEAKING_THIS!>withGetter<!>.hashCode()
1515
}
1616

17-
val other = <!UNINITIALIZED_VARIABLE!>delegated<!>
17+
val other = <!UNINITIALIZED_VARIABLE, DEBUG_INFO_LEAKING_THIS!>delegated<!>
1818

1919
val another = <!UNINITIALIZED_VARIABLE!>simple<!>
2020

21-
val something = withGetter
21+
val something = <!DEBUG_INFO_LEAKING_THIS!>withGetter<!>
2222

2323
val delegated: String by CustomDelegate()
2424

@@ -28,5 +28,5 @@ class Kaboom() {
2828
get() = "abc"
2929

3030
// No error should be here
31-
val after = delegated
31+
val after = <!DEBUG_INFO_LEAKING_THIS!>delegated<!>
3232
}

compiler/testData/diagnostics/testsWithStdLib/PropertyDelegateWithPrivateSet.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class My {
55
private set
66

77
// Error: Variable 'delegate' must be initialized
8-
val another: String = delegate
8+
val another: String = <!DEBUG_INFO_LEAKING_THIS!>delegate<!>
99

1010
var delegateWithBackingField: String by kotlin.properties.Delegates.notNull()
1111
<!ACCESSOR_FOR_DELEGATED_PROPERTY!>private set(arg) { field = arg }<!>

compiler/tests/org/jetbrains/kotlin/checkers/DiagnosticsTestGenerated.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2913,6 +2913,12 @@ public void testAssignment() throws Exception {
29132913
doTest(fileName);
29142914
}
29152915

2916+
@TestMetadata("backing.kt")
2917+
public void testBacking() throws Exception {
2918+
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/constructorConsistency/backing.kt");
2919+
doTest(fileName);
2920+
}
2921+
29162922
@TestMetadata("basic.kt")
29172923
public void testBasic() throws Exception {
29182924
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/constructorConsistency/basic.kt");
@@ -2949,12 +2955,24 @@ public void testDerivedProperty() throws Exception {
29492955
doTest(fileName);
29502956
}
29512957

2958+
@TestMetadata("getset.kt")
2959+
public void testGetset() throws Exception {
2960+
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/constructorConsistency/getset.kt");
2961+
doTest(fileName);
2962+
}
2963+
29522964
@TestMetadata("init.kt")
29532965
public void testInit() throws Exception {
29542966
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/constructorConsistency/init.kt");
29552967
doTest(fileName);
29562968
}
29572969

2970+
@TestMetadata("initwithgetter.kt")
2971+
public void testInitwithgetter() throws Exception {
2972+
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/constructorConsistency/initwithgetter.kt");
2973+
doTest(fileName);
2974+
}
2975+
29582976
@TestMetadata("lambdaInObject.kt")
29592977
public void testLambdaInObject() throws Exception {
29602978
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/diagnostics/tests/constructorConsistency/lambdaInObject.kt");

0 commit comments

Comments
 (0)