Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import com.jetbrains.php.lang.psi.PhpPsiElementFactory
import com.jetbrains.php.lang.psi.elements.Field
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.Parameter
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.jetbrains.php.lang.psi.elements.PhpNamedElement
import com.vk.kphpstorm.helpers.parentDocComment
import com.vk.kphpstorm.kphptags.KphpDocTag
import com.vk.kphpstorm.kphptags.KphpSerializedFieldDocTag

/**
Expand Down Expand Up @@ -68,6 +70,13 @@ object PhpDocPsiBuilder {
return field.docComment!!.addTag(project, "@var", "type")
}

fun addTagToClass(klass: PhpClass, annotation: KphpDocTag): PhpDocTag {
val project = klass.project
val docComment = klass.docComment ?: createDocComment(project, klass)

return docComment.transformToMultiline(project).addTag(project, annotation.nameWithAt)
}


/**
* Create empty phpdoc and insert it before function/class/field
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.vk.kphpstorm.intentions

import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import com.intellij.psi.util.findParentInFile
import com.jetbrains.php.lang.psi.elements.AssignmentExpression
import com.jetbrains.php.lang.psi.elements.PhpClass
import com.vk.kphpstorm.inspections.helpers.PhpDocPsiBuilder
import com.vk.kphpstorm.kphptags.KphpImmutableClassDocTag

class AddImmutableClassAnnotationIntention : PsiElementBaseIntentionAction() {
override fun getText(): String = "Add @kphp-immutable-class"
override fun getFamilyName(): String = getText()

override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
if (!element.isClassNameNode()) {
return false
}

val klass = element.parent as PhpClass
if (klass.isAbstract || klass.isInterface || klass.isTrait || klass.isAnonymous) {
return false
}

val klassDocNode = klass.docComment

// do not suggest if already present
if (klassDocNode != null && KphpImmutableClassDocTag.existsInDocComment(klassDocNode)) {
return false
}

return !isClassLocallyImmutable(klass)
}

/**
* Simple local class mutability check. If there is any field mutation in class,
* the class is mutable. The only exception is the class constructor
*/
private fun isClassLocallyImmutable(klass: PhpClass): Boolean {
val searchScope = LocalSearchScope(klass)
for (field in klass.fields) {
val hasAnyMutation = ReferencesSearch.search(field, searchScope).any { ref ->
val element = ref.element

isMutatingOp(element) && !isInClassConstructor(klass, element)
}

if (hasAnyMutation) {
return true
}
}

return false
}

private fun isMutatingOp(psiElement: PsiElement): Boolean {
val parent = psiElement.parent

return parent is AssignmentExpression && parent.variable == psiElement
}

private fun isInClassConstructor(klass: PhpClass, psiElement: PsiElement): Boolean {
val classConstructor = klass.constructor
return classConstructor != null && psiElement.findParentInFile { e -> e == classConstructor } != null
}

override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
val klass = element.parent as PhpClass
PhpDocPsiBuilder.addTagToClass(klass, KphpImmutableClassDocTag)
}

private fun PsiElement.isClassNameNode(): Boolean {
val klass = this.parent as? PhpClass ?: return false
return klass.nameIdentifier == this
}
}
7 changes: 6 additions & 1 deletion src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,12 @@
<language>PHP</language>
<className>com.vk.kphpstorm.intentions.PrettifyPhpdocBlockIntention</className>
<category>PHP</category>
<descriptionDirectoryName>PrettifyPhpdocBlockIntention</descriptionDirectoryName>
</intentionAction>

<intentionAction>
<language>PHP</language>
<className>com.vk.kphpstorm.intentions.AddImmutableClassAnnotationIntention</className>
<category>PHP</category>
</intentionAction>

<additionalTextAttributes scheme="Default" file="colorSchemes/KphpAddonsDefault.xml"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* My favorite class
* <spot>@kphp-immutable-class</spot>
*/
class MyClass {
// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* My favorite class
*/
class MyClass {
// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add <i>@kphp-immutable-class annotation</i> to the class
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

class <caret>C1 {
// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

/**
* @kphp-immutable-class
*/
class C1 {
// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

/**
* @kphp-serializable
* @kphp-color slow-ignore
*/
class <caret>C1 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

/**
* @kphp-immutable-class
* @kphp-serializable
* @kphp-color slow-ignore
*/
class <caret>C1 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

abstract class <caret>C1 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

interface <caret>C1 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

trait <caret>C1 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

/**
* @kphp-immutable-class
*/
class <caret>C1 {
// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

class C1 extends <caret>C2 {
// ...
}

class C2 {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

class C1 {
<caret>
// ...
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

class <caret>C1 {
public int $field;

public function foo() {
$this->field = 1; // mutate the field
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

class <caret>C1 {
public int $field;

public function __construct(int $arg)
{
$this->field = $arg; // field mutation in constructor, that's ok
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

/**
* @kphp-immutable-class
*/
class <caret>C1 {
public int $field;

public function __construct(int $arg)
{
$this->field = $arg; // field mutation in constructor, that's ok
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

class <caret>C1 {
public int $field;
}

function foo()
{
$v = new C1();
$v->field = 1; // mutation outside of a class, that's ok
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

/**
* @kphp-immutable-class
*/
class <caret>C1 {
public int $field;
}

function foo()
{
$v = new C1();
$v->field = 1; // mutation outside of a class, that's ok
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

class <caret>C1 {
public int $field = 1;

public function foo() {
$tmp = $this->field; // no field mutation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

/**
* @kphp-immutable-class
*/
class <caret>C1 {
public int $field = 1;

public function foo() {
$tmp = $this->field; // no field mutation
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

/** @kphp-serializable */
class <caret>C1 { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

/**
* @kphp-immutable-class
* @kphp-serializable
*/
class <caret>C1 { }
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,19 @@ abstract class IntentionTestBase(
val qfFile = fixtureFile.replace(".fixture.php", ".qf.php")
myFixture.checkResultByFile(qfFile)
}

/**
* Assert there are no intention [intentionToExecute] in file [fixtureFile]
*/
protected fun assertNoIntention(fixtureFile: String) {
setupLanguageLevel()

KphpStormConfiguration.saveThatSetupForProjectDone(project)
myFixture.configureByFile(fixtureFile)
val availableIntentions = myFixture
.availableIntentions
.filter { intentionAction -> intentionAction.familyName == intentionToExecute.familyName }

assertEmpty(availableIntentions)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.vk.kphpstorm.testing.tests

import com.vk.kphpstorm.intentions.AddImmutableClassAnnotationIntention
import com.vk.kphpstorm.testing.infrastructure.IntentionTestBase

class AddImmutableAnnoIntentionTest : IntentionTestBase(AddImmutableClassAnnotationIntention()) {
fun testAddImmutableAnno1() {
runIntention("kphp_intentions/immutable_class_intention-1.fixture.php")
}

fun testAddImmutableAnno2() {
assertNoIntention("kphp_intentions/immutable_class_intention-2.nointention.php")
}

fun testAddImmutableAnno3() {
assertNoIntention("kphp_intentions/immutable_class_intention-3.nointention.php")
}

fun testAddImmutableAnno4() {
assertNoIntention("kphp_intentions/immutable_class_intention-4.nointention.php")
}

fun testAddImmutableAnno5() {
assertNoIntention("kphp_intentions/immutable_class_intention-5.nointention.php")
}

fun testAddImmutableAnno6() {
runIntention("kphp_intentions/immutable_class_intention-6.fixture.php")
}

fun testAddImmutableAnno7() {
runIntention("kphp_intentions/immutable_class_intention-7.fixture.php")
}

fun testAddImmutableAnno8() {
runIntention("kphp_intentions/immutable_class_intention-8.fixture.php")
}

fun testAddImmutableAnno9() {
runIntention("kphp_intentions/immutable_class_intention-9.fixture.php")
}

fun testAddImmutableAnno10() {
runIntention("kphp_intentions/immutable_class_intention-10.fixture.php")
}

fun testAddImmutableAnno11() {
assertNoIntention("kphp_intentions/immutable_class_intention-11.nointent.php")
}

fun testAddImmutableAnno12() {
assertNoIntention("kphp_intentions/immutable_class_intention-12.nointent.php")
}

fun testAddImmutableAnno13() {
assertNoIntention("kphp_intentions/immutable_class_intention-13.nointent.php")
}
}
Loading