diff --git a/idea/resources/inspectionDescriptions/RemoveUnnecessaryLateinit.html b/idea/resources/inspectionDescriptions/RemoveUnnecessaryLateinit.html
new file mode 100644
index 00000000000..2d6647dd968
--- /dev/null
+++ b/idea/resources/inspectionDescriptions/RemoveUnnecessaryLateinit.html
@@ -0,0 +1,5 @@
+
+
+This inspection detects lateinit that can be safely removed
+
+
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/after.kt.template b/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/after.kt.template
new file mode 100644
index 00000000000..5b7cd1e2379
--- /dev/null
+++ b/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/after.kt.template
@@ -0,0 +1,7 @@
+class My {
+ var x: String
+
+ init {
+ x = "My"
+ }
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/before.kt.template b/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/before.kt.template
new file mode 100644
index 00000000000..7924d662dbf
--- /dev/null
+++ b/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/before.kt.template
@@ -0,0 +1,7 @@
+class My {
+ lateinit var x: String
+
+ init {
+ x = "My"
+ }
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/description.html b/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/description.html
new file mode 100644
index 00000000000..b01afecd641
--- /dev/null
+++ b/idea/resources/intentionDescriptions/RemoveUnnecessaryLateinitIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention removes unnecessary lateinit modifiers.
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index 5b3cb0709ce..4d59a799235 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1273,6 +1273,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.RemoveUnnecessaryLateinitIntention
+ Kotlin
+
+
org.jetbrains.kotlin.idea.intentions.RemoveSingleExpressionStringTemplateIntention
Kotlin
@@ -1641,6 +1646,14 @@
language="kotlin"
/>
+
+
(RemoveUnnecessaryLateinitIntention()) {
+ override val problemHighlightType = ProblemHighlightType.LIKE_UNUSED_SYMBOL
+
+ override val problemText = "Unnecessary lateinit"
+}
+
+class RemoveUnnecessaryLateinitIntention : SelfTargetingRangeIntention(KtProperty::class.java, "Remove unnecessary lateinit") {
+ override fun applicabilityRange(element: KtProperty): TextRange? {
+ if (!element.hasModifier(KtTokens.LATEINIT_KEYWORD)) return null
+ val ktClass = element.getStrictParentOfType() ?: return null
+ if (ktClass.getAnonymousInitializers().any { hasAssignmentStatements(it.body, element) }) {
+ return element.lateinitTextRange()
+ }
+ if (ktClass.hasPrimaryConstructor()) return null
+ val secondaryConstructors = ktClass.getSecondaryConstructors()
+
+ val secondaryConstructorsInfo = secondaryConstructors.map { secondaryConstructor ->
+ val delegationCall = secondaryConstructor.getDelegationCall()
+ val hasAssignmentStatements = hasAssignmentStatements(secondaryConstructor.bodyExpression, element)
+ if (!hasAssignmentStatements && !delegationCall.isCallToThis) return null
+ val resolvedCall = delegationCall.getResolvedCall(delegationCall.analyzeFully()) ?: return null
+ val delegationCallIndex = secondaryConstructors.indexOfFirst {
+ DescriptorToSourceUtils.descriptorToDeclaration(resolvedCall.resultingDescriptor) == it
+ }
+ SecondaryConstructorInfo(hasAssignmentStatements, delegationCallIndex)
+ }
+ return if (assignmentReachableFromAnySecondaryConstructor(secondaryConstructorsInfo)) element.lateinitTextRange()
+ else null
+ }
+
+ private fun assignmentReachableFromAnySecondaryConstructor(constructorsInfo: List): Boolean {
+ constructorsInfo.forEach {
+ val visitedInfo = hashSetOf()
+ var currentConstructor = it
+ while (!currentConstructor.hasAssignmentStatement) {
+ if (!visitedInfo.add(currentConstructor)) return false
+ currentConstructor = constructorsInfo.getOrNull(currentConstructor.delegationCallIndex) ?: return false
+ }
+ }
+ return true
+ }
+
+ private fun KtProperty.lateinitTextRange() = modifierList?.getModifier(KtTokens.LATEINIT_KEYWORD)?.range
+
+ private fun hasAssignmentStatements(body: KtExpression?, property: KtProperty) =
+ body?.children?.any {
+ it is KtBinaryExpression &&
+ it.operationToken === KtTokens.EQ &&
+ it.left?.text == property.name
+ } ?: false
+
+ override fun applyTo(element: KtProperty, editor: Editor?) {
+ element.removeModifier(KtTokens.LATEINIT_KEYWORD)
+ }
+}
+
+private data class SecondaryConstructorInfo(val hasAssignmentStatement: Boolean, val delegationCallIndex: Int)
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/.intention b/idea/testData/intentions/removeUnnecessaryLateinit/.intention
new file mode 100644
index 00000000000..f6e91edcea8
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.RemoveUnnecessaryLateinitIntention
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt
new file mode 100644
index 00000000000..8721d280e41
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt
@@ -0,0 +1,9 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ lateinit var bar: String
+
+ constructor(baz: Int) {
+ bar = ""
+ }
+}
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt.after b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt.after
new file mode 100644
index 00000000000..b731a7b979c
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt.after
@@ -0,0 +1,9 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ var bar: String
+
+ constructor(baz: Int) {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithErroneousDelegation.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithErroneousDelegation.kt
new file mode 100644
index 00000000000..3d660fe7e1e
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithErroneousDelegation.kt
@@ -0,0 +1,12 @@
+// IS_APPLICABLE: false
+// ERROR: None of the following functions can be called with the arguments supplied:
public constructor Foo() defined in Foo
public constructor Foo(x: String, y: String) defined in Foo
+
+class Foo {
+ lateinit var x: String
+
+ constructor() {
+ x = "Foo"
+ }
+
+ constructor(x: String, y: String): this(y.hashCode())
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt
new file mode 100644
index 00000000000..3edcc6dea88
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt
@@ -0,0 +1,9 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ lateinit var bar: String
+
+ init {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt.after b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt.after
new file mode 100644
index 00000000000..8e75eb92fce
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt.after
@@ -0,0 +1,9 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ var bar: String
+
+ init {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt
new file mode 100644
index 00000000000..f8bc360ff75
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt
@@ -0,0 +1,13 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ lateinit var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(baz: Int) {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt.after b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt.after
new file mode 100644
index 00000000000..a697dc99bc3
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt.after
@@ -0,0 +1,13 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(baz: Int) {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt
new file mode 100644
index 00000000000..62551652dbb
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt
@@ -0,0 +1,15 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ lateinit var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(a: Int) : this() {
+ }
+
+ constructor(a: Int, b: Int) : this(a) {
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt.after b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt.after
new file mode 100644
index 00000000000..98f2d958c46
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt.after
@@ -0,0 +1,15 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(a: Int) : this() {
+ }
+
+ constructor(a: Int, b: Int) : this(a) {
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt
new file mode 100644
index 00000000000..15de7563f5c
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt
@@ -0,0 +1,14 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ lateinit var bar: String
+ var baz: Int
+
+ init {
+ baz = 1
+ }
+
+ init {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt.after b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt.after
new file mode 100644
index 00000000000..b97dd3391e8
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt.after
@@ -0,0 +1,14 @@
+// INTENTION_TEXT: Remove unnecessary lateinit
+
+class Foo {
+ var bar: String
+ var baz: Int
+
+ init {
+ baz = 1
+ }
+
+ init {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPlusAssign.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPlusAssign.kt
new file mode 100644
index 00000000000..9f79369317c
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPlusAssign.kt
@@ -0,0 +1,9 @@
+// IS_APPLICABLE: false
+
+class Foo {
+ lateinit var bar: String
+
+ constructor(baz: Int) {
+ bar += baz
+ }
+}
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPrimaryConstructorAndConstructor.kt b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPrimaryConstructorAndConstructor.kt
new file mode 100644
index 00000000000..7fb92d4e2e9
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPrimaryConstructorAndConstructor.kt
@@ -0,0 +1,9 @@
+// IS_APPLICABLE: false
+
+class Foo() {
+ lateinit var bar: String
+
+ constructor(baz: Int) : this() {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/normalLateinit.kt b/idea/testData/intentions/removeUnnecessaryLateinit/normalLateinit.kt
new file mode 100644
index 00000000000..b832a36b4b0
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/normalLateinit.kt
@@ -0,0 +1,9 @@
+// IS_APPLICABLE: false
+
+class Foo {
+ lateinit var bar: String
+
+ fun init() {
+ bar = ""
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt
new file mode 100644
index 00000000000..fd8139b4dd3
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt
@@ -0,0 +1,14 @@
+// IS_APPLICABLE: true
+// ERROR: There's a cycle in the delegation calls chain
+
+class Foo {
+ lateinit var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(a: Int) : this(a) {
+ bar = "a"
+ }
+}
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt.after b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt.after
new file mode 100644
index 00000000000..bfc1aa5e888
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt.after
@@ -0,0 +1,14 @@
+// IS_APPLICABLE: true
+// ERROR: There's a cycle in the delegation calls chain
+
+class Foo {
+ var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(a: Int) : this(a) {
+ bar = "a"
+ }
+}
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateLoop.kt b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateLoop.kt
new file mode 100644
index 00000000000..b2dc4b78cbc
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateLoop.kt
@@ -0,0 +1,21 @@
+// IS_APPLICABLE: false
+// ERROR: There's a cycle in the delegation calls chain
+// ERROR: There's a cycle in the delegation calls chain
+// ERROR: There's a cycle in the delegation calls chain
+
+class Foo {
+ lateinit var bar: String
+
+ constructor() {
+ bar = ""
+ }
+
+ constructor(a: Int) : this(a, 0, 0) {
+ }
+
+ constructor(a: Int, b: Int) : this(a) {
+ }
+
+ constructor(a: Int, b: Int, c: Int) : this(a, b) {
+ }
+}
diff --git a/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateSuper.kt b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateSuper.kt
new file mode 100644
index 00000000000..bae359bf99d
--- /dev/null
+++ b/idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateSuper.kt
@@ -0,0 +1,14 @@
+// IS_APPLICABLE: false
+
+open class Bar(val a: Int = 0)
+
+class Foo : Bar {
+ lateinit var bar: String
+
+ constructor() : super() {
+ bar = ""
+ }
+
+ constructor(a: Int) : super(a) {
+ }
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
index df90a370410..2594b39009e 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
@@ -8531,6 +8531,87 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/removeUnnecessaryLateinit")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class RemoveUnnecessaryLateinit extends AbstractIntentionTest {
+ public void testAllFilesPresentInRemoveUnnecessaryLateinit() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/removeUnnecessaryLateinit"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("lateinitWithConstructor.kt")
+ public void testLateinitWithConstructor() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithConstructor.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithErroneousDelegation.kt")
+ public void testLateinitWithErroneousDelegation() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithErroneousDelegation.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithInit.kt")
+ public void testLateinitWithInit() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithInit.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithMultipleConstructors.kt")
+ public void testLateinitWithMultipleConstructors() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructors.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithMultipleConstructorsAndDelegation.kt")
+ public void testLateinitWithMultipleConstructorsAndDelegation() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleConstructorsAndDelegation.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithMultipleInit.kt")
+ public void testLateinitWithMultipleInit() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithMultipleInit.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithPlusAssign.kt")
+ public void testLateinitWithPlusAssign() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPlusAssign.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("lateinitWithPrimaryConstructorAndConstructor.kt")
+ public void testLateinitWithPrimaryConstructorAndConstructor() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/lateinitWithPrimaryConstructorAndConstructor.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("normalLateinit.kt")
+ public void testNormalLateinit() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/normalLateinit.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("secondaryConstructorDelegateItself.kt")
+ public void testSecondaryConstructorDelegateItself() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateItself.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("secondaryConstructorDelegateLoop.kt")
+ public void testSecondaryConstructorDelegateLoop() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateLoop.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("secondaryConstructorDelegateSuper.kt")
+ public void testSecondaryConstructorDelegateSuper() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/removeUnnecessaryLateinit/secondaryConstructorDelegateSuper.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/removeUnnecessaryParentheses")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)