diff --git a/idea/resources/inspectionDescriptions/RecursivePropertyAccessor.html b/idea/resources/inspectionDescriptions/RecursivePropertyAccessor.html
new file mode 100644
index 00000000000..a084dc3d550
--- /dev/null
+++ b/idea/resources/inspectionDescriptions/RecursivePropertyAccessor.html
@@ -0,0 +1,23 @@
+
+
+Reports recursive property accessor calls which can end up with StackOverflowError
+
+ For example:
+
+ class A {
+ var x = 0
+ get() {
+ return x //recursive getter call
+ }
+
+ var y = 0
+ set(value) {
+ if (value > 0) {
+ y = value //recursive setter call
+ }
+ }
+ }
+
+
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index 9760e57ae37..2475ae6d2b9 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -2172,6 +2172,14 @@
language="kotlin"
/>
+
+
diff --git a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt
index 0945d78f212..27a55db7eaf 100644
--- a/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt
+++ b/idea/src/org/jetbrains/kotlin/idea/highlighter/KotlinRecursiveCallLineMarkerProvider.kt
@@ -27,6 +27,7 @@ import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.SimpleFunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
+import org.jetbrains.kotlin.idea.inspections.RecursivePropertyAccessorInspection
import org.jetbrains.kotlin.idea.util.getThisReceiverOwner
import org.jetbrains.kotlin.lexer.KtToken
import org.jetbrains.kotlin.name.Name
@@ -75,6 +76,8 @@ class KotlinRecursiveCallLineMarkerProvider : LineMarkerProvider {
}
private fun isRecursiveCall(element: KtElement): Boolean {
+ if (RecursivePropertyAccessorInspection.isRecursivePropertyAccess(element)) return true
+ if (RecursivePropertyAccessorInspection.isRecursiveSyntheticPropertyAccess(element)) return true
// Fast check for names without resolve
val resolveName = getCallNameFromPsi(element) ?: return false
val enclosingFunction = getEnclosingFunction(element, false) ?: return false
diff --git a/idea/src/org/jetbrains/kotlin/idea/inspections/RecursivePropertyAccessorInspection.kt b/idea/src/org/jetbrains/kotlin/idea/inspections/RecursivePropertyAccessorInspection.kt
new file mode 100644
index 00000000000..eaa2d628d2f
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/inspections/RecursivePropertyAccessorInspection.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2010-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.kotlin.idea.inspections
+
+import com.intellij.codeInspection.*
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElementVisitor
+import com.intellij.psi.util.PsiTreeUtil
+import org.jetbrains.kotlin.idea.caches.resolve.analyze
+import org.jetbrains.kotlin.lexer.KtTokens
+import org.jetbrains.kotlin.psi.*
+import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
+import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
+import org.jetbrains.kotlin.resolve.BindingContext
+import org.jetbrains.kotlin.resolve.BindingContext.DECLARATION_TO_DESCRIPTOR
+import org.jetbrains.kotlin.resolve.BindingContext.REFERENCE_TARGET
+import org.jetbrains.kotlin.synthetic.SyntheticJavaPropertyDescriptor
+
+class RecursivePropertyAccessorInspection : AbstractKotlinInspection() {
+
+ override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
+ return object : KtVisitorVoid() {
+ override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
+ super.visitSimpleNameExpression(expression)
+ if (isRecursivePropertyAccess(expression)) {
+ holder.registerProblem(expression,
+ "Recursive property accessor",
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
+ ReplaceWithFieldFix())
+ }
+ else if (isRecursiveSyntheticPropertyAccess(expression)) {
+ holder.registerProblem(expression,
+ "Recursive synthetic property accessor",
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
+ }
+ }
+ }
+ }
+
+ class ReplaceWithFieldFix : LocalQuickFix {
+
+ override fun getName() = "Replace with 'field'"
+
+ override fun getFamilyName() = name
+
+ override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
+ val expression = descriptor.psiElement as KtExpression
+ val factory = KtPsiFactory(expression)
+ expression.replace(factory.createExpression("field"))
+ }
+ }
+
+ companion object {
+
+ private fun KtBinaryExpression?.isAssignmentTo(expression: KtSimpleNameExpression): Boolean =
+ this != null && KtPsiUtil.isAssignment(this) && PsiTreeUtil.isAncestor(left, expression, false)
+
+ private fun isSameAccessor(expression: KtSimpleNameExpression, isGetter: Boolean): Boolean {
+ val binaryExpr = expression.getStrictParentOfType()
+ if (isGetter) {
+ if (binaryExpr.isAssignmentTo(expression)) {
+ return KtTokens.AUGMENTED_ASSIGNMENTS.contains(binaryExpr?.operationToken)
+ }
+ return true
+ }
+ else /* isSetter */ {
+ if (binaryExpr.isAssignmentTo(expression)) {
+ return true
+ }
+ val unaryExpr = expression.getStrictParentOfType()
+ if (unaryExpr?.operationToken.let { it == KtTokens.PLUSPLUS || it == KtTokens.MINUSMINUS }) {
+ return true
+ }
+ }
+ return false
+ }
+
+ fun isRecursivePropertyAccess(element: KtElement): Boolean {
+ if (element !is KtSimpleNameExpression) return false
+ val propertyAccessor = element.getParentOfType(true) as? KtPropertyAccessor ?: return false
+ if (element.text != propertyAccessor.property.name) return false
+ val bindingContext = element.analyze()
+ val target = bindingContext[REFERENCE_TARGET, element]
+ if (target != bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, propertyAccessor.property]) return false
+ return isSameAccessor(element, propertyAccessor.isGetter)
+ }
+
+ fun isRecursiveSyntheticPropertyAccess(element: KtElement): Boolean {
+ if (element !is KtSimpleNameExpression) return false
+ val namedFunction = element.getParentOfType(true) as? KtNamedFunction ?: return false
+ val name = namedFunction.name ?: return false
+ val referencedName = element.text.capitalize()
+ val isGetter = name == "get$referencedName"
+ val isSetter = name == "set$referencedName"
+ if (!isGetter && !isSetter) return false
+ val bindingContext = element.analyze()
+ val syntheticDescriptor = bindingContext[REFERENCE_TARGET, element] as? SyntheticJavaPropertyDescriptor ?: return false
+ val namedFunctionDescriptor = bindingContext[DECLARATION_TO_DESCRIPTOR, namedFunction]
+ if (namedFunctionDescriptor != syntheticDescriptor.getMethod &&
+ namedFunctionDescriptor != syntheticDescriptor.setMethod) return false
+ return isSameAccessor(element, isGetter)
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/codeInsight/lineMarker/recursiveCall/propertyAccessors.kt b/idea/testData/codeInsight/lineMarker/recursiveCall/propertyAccessors.kt
new file mode 100644
index 00000000000..89cb9598dfb
--- /dev/null
+++ b/idea/testData/codeInsight/lineMarker/recursiveCall/propertyAccessors.kt
@@ -0,0 +1,27 @@
+class A {
+ var x = 0
+ set(value) {
+ x++
+ x+=1
+ x = value
+ }
+
+ var y = 0
+ get() {
+ println("$y")
+ y++
+ y += 1
+ return y
+ }
+
+ var z = 0
+ get() = z
+
+ var field = 0
+ get() {
+ return if (field != 0) field else -1
+ }
+ set(value) {
+ if (value >= 0) field = value
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/inspections/recursivePropertyAccessor/a/JavaInterface.java b/idea/testData/inspections/recursivePropertyAccessor/a/JavaInterface.java
new file mode 100644
index 00000000000..a1613d29f1d
--- /dev/null
+++ b/idea/testData/inspections/recursivePropertyAccessor/a/JavaInterface.java
@@ -0,0 +1,7 @@
+package a;
+
+public interface JavaInterface {
+ String getSomething();
+
+ void setSomething(String value);
+}
diff --git a/idea/testData/inspections/recursivePropertyAccessor/inspectionData/expected.xml b/idea/testData/inspections/recursivePropertyAccessor/inspectionData/expected.xml
new file mode 100644
index 00000000000..0dbf3ec6ef5
--- /dev/null
+++ b/idea/testData/inspections/recursivePropertyAccessor/inspectionData/expected.xml
@@ -0,0 +1,119 @@
+
+
+ recursivePropertyAccessors.kt
+ 4
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 5
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 6
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 11
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 12
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 13
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 14
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 18
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursiveSyntheticPropertyAccessor.kt
+ 9
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive synthetic property accessor
+
+
+
+ recursiveSyntheticPropertyAccessor.kt
+ 14
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive synthetic property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 27
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 31
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
+ recursivePropertyAccessors.kt
+ 38
+ light_idea_test_case
+
+ Recursive property accessor
+ Recursive property accessor
+
+
+
diff --git a/idea/testData/inspections/recursivePropertyAccessor/inspectionData/inspections.test b/idea/testData/inspections/recursivePropertyAccessor/inspectionData/inspections.test
new file mode 100644
index 00000000000..ea94c506ab1
--- /dev/null
+++ b/idea/testData/inspections/recursivePropertyAccessor/inspectionData/inspections.test
@@ -0,0 +1 @@
+// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.RecursivePropertyAccessorInspection
\ No newline at end of file
diff --git a/idea/testData/inspections/recursivePropertyAccessor/recursivePropertyAccessors.kt b/idea/testData/inspections/recursivePropertyAccessor/recursivePropertyAccessors.kt
new file mode 100644
index 00000000000..ba6820401e3
--- /dev/null
+++ b/idea/testData/inspections/recursivePropertyAccessor/recursivePropertyAccessors.kt
@@ -0,0 +1,41 @@
+class A {
+ var x = 0
+ set(value) {
+ x++
+ x += 1
+ x = value
+ }
+
+ var y = 0
+ get() {
+ println("$y")
+ y++
+ y += 1
+ return y
+ }
+
+ var z = 0
+ get() = z
+
+ var w
+ set(value) {
+ field = w + value
+ }
+
+ var field = 0
+ get() {
+ this.field
+ return if (field != 0) field else -1
+ }
+ set(value) {
+ this.field = value
+ if (value >= 0) field = value
+ }
+
+ companion object {
+ var g = 0
+ set(value) {
+ A.g = 99
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/inspections/recursivePropertyAccessor/recursiveSyntheticPropertyAccessor.kt b/idea/testData/inspections/recursivePropertyAccessor/recursiveSyntheticPropertyAccessor.kt
new file mode 100644
index 00000000000..2fab7848907
--- /dev/null
+++ b/idea/testData/inspections/recursivePropertyAccessor/recursiveSyntheticPropertyAccessor.kt
@@ -0,0 +1,17 @@
+import a.JavaInterface
+
+class B {
+ var something: String = "123"
+
+ class Nested : JavaInterface {
+ override fun setSomething(value: String) {
+ val x = something // OK
+ something = value
+ }
+
+ override fun getSomething(): String {
+ something = "456" // OK
+ return something
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java
index 09f375314a1..5fd09d9853a 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/InspectionTestGenerated.java
@@ -233,6 +233,12 @@ public class InspectionTestGenerated extends AbstractInspectionTest {
doTest(fileName);
}
+ @TestMetadata("recursivePropertyAccessor/inspectionData/inspections.test")
+ public void testRecursivePropertyAccessor_inspectionData_Inspections_test() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/recursivePropertyAccessor/inspectionData/inspections.test");
+ doTest(fileName);
+ }
+
@TestMetadata("redundantIf/inspectionData/inspections.test")
public void testRedundantIf_inspectionData_Inspections_test() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/inspections/redundantIf/inspectionData/inspections.test");
diff --git a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/LineMarkersTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/LineMarkersTestGenerated.java
index 13a606ddb7f..bad67c21789 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/codeInsight/LineMarkersTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/codeInsight/LineMarkersTestGenerated.java
@@ -281,6 +281,12 @@ public class LineMarkersTestGenerated extends AbstractLineMarkersTest {
doTest(fileName);
}
+ @TestMetadata("propertyAccessors.kt")
+ public void testPropertyAccessors() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/lineMarker/recursiveCall/propertyAccessors.kt");
+ doTest(fileName);
+ }
+
@TestMetadata("severalCallsInOneLine.kt")
public void testSeveralCallsInOneLine() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/codeInsight/lineMarker/recursiveCall/severalCallsInOneLine.kt");