Skip to content

Commit

Permalink
Merge pull request #86 from Haehnchen/feature/depreaction-mock
Browse files Browse the repository at this point in the history
add inspection for mocked deprecation methods
  • Loading branch information
Haehnchen committed Mar 2, 2023
2 parents 08c148b + dc6e3e8 commit d30152a
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package de.espend.idea.php.phpunit.inspection;

import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.psi.elements.*;
import de.espend.idea.php.phpunit.utils.PatternUtil;
import de.espend.idea.php.phpunit.utils.PhpElementsUtil;
import de.espend.idea.php.phpunit.utils.PhpUnitPluginUtil;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.Objects;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class DeprecatedMockedMethodInspection extends LocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new PsiElementVisitor() {
@Override
public void visitElement(@NotNull PsiElement psiElement) {
if (!(psiElement instanceof StringLiteralExpression)) {
super.visitElement(psiElement);
return;
}

if (PatternUtil.getMethodReferenceWithParameterPattern().accepts(psiElement)) {
visitCreateMock((StringLiteralExpression) psiElement, holder);
}

if (PatternUtil.getArrayParameterPattern().accepts(psiElement)) {
visitCreatePartialMock((StringLiteralExpression) psiElement, holder);
}

super.visitElement(psiElement);
}
};
}
private static void visitCreateMock(@NotNull StringLiteralExpression psiElement, @NotNull ProblemsHolder holder) {
String clazz = PhpUnitPluginUtil.findCreateMockParameterOnParameterScope(psiElement);
if (clazz == null) {
return;
}

String method = psiElement.getContents();
if (!method.isBlank()) {
registerProblemIfDeprecated(psiElement, holder, clazz, method);
}
}

private static void registerProblemIfDeprecated(@NotNull StringLiteralExpression psiElement, @NotNull ProblemsHolder holder, @NotNull String clazz, @NotNull String method) {
boolean isDeprecated = PhpIndex.getInstance(psiElement.getProject())
.getAnyByFQN(clazz).stream()
.map(phpClass -> phpClass.findMethodByName(method))
.filter(Objects::nonNull)
.anyMatch(PhpNamedElement::isDeprecated);

if (isDeprecated) {
holder.registerProblem(psiElement, "Method '" + method + "' is deprecated ", ProblemHighlightType.LIKE_DEPRECATED);
}
}

private static void visitCreatePartialMock(@NotNull StringLiteralExpression psiElement, @NotNull ProblemsHolder holder) {
MethodReference parentOfType = PsiTreeUtil.getParentOfType(psiElement, MethodReference.class);
if (parentOfType == null) {
return;
}

if (PhpUnitPluginUtil.isCreatePartialMockMethod(parentOfType)) {
PsiElement originalClassName = parentOfType.getParameter("originalClassName", 0);

if (originalClassName == null) {
return;
}

String method = psiElement.getContents();
if (method.isBlank()) {
return;
}

String clazz = PhpElementsUtil.getStringValue(originalClassName);
if (StringUtils.isBlank(clazz)) {
return;
}

registerProblemIfDeprecated(psiElement, holder, clazz, method);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import com.intellij.codeInsight.completion.*;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.PsiElementPattern.Capture;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ProcessingContext;
import com.jetbrains.php.lang.psi.elements.*;
import de.espend.idea.php.phpunit.utils.PatternUtil;
import de.espend.idea.php.phpunit.utils.PhpElementsUtil;
import de.espend.idea.php.phpunit.utils.PhpUnitPluginUtil;
import org.apache.commons.lang.StringUtils;
Expand All @@ -20,7 +20,7 @@
public class PhpUnitCreatePartialMock {
public static class Completion extends CompletionContributor {
public Completion() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(getArrayParameterPattern()), new CompletionProvider<>() {
extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(PatternUtil.getArrayParameterPattern()), new CompletionProvider<>() {
@Override
protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) {
PsiElement psiElement1 = completionParameters.getPosition();
Expand All @@ -35,9 +35,7 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters
return;
}

if(PhpElementsUtil.isMethodReferenceInstanceOf(parentOfType, "\\PHPUnit\\Framework\\TestCase", "createPartialMock") ||
PhpElementsUtil.isMethodReferenceInstanceOf(parentOfType, "PHPUnit_Framework_TestCase", "createPartialMock")
) {
if (PhpUnitPluginUtil.isCreatePartialMockMethod(parentOfType)) {
PsiElement originalClassName = parentOfType.getParameter("originalClassName", 0);

if (originalClassName == null) {
Expand All @@ -59,7 +57,7 @@ protected void addCompletions(@NotNull CompletionParameters completionParameters
public static class ReferenceContributor extends PsiReferenceContributor {
@Override
public void registerReferenceProviders(@NotNull PsiReferenceRegistrar psiReferenceRegistrar) {
psiReferenceRegistrar.registerReferenceProvider(getArrayParameterPattern(),
psiReferenceRegistrar.registerReferenceProvider(PatternUtil.getArrayParameterPattern(),
new PsiReferenceProvider() {
@NotNull
@Override
Expand All @@ -78,9 +76,7 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar psiReferen
return new PsiReference[0];
}

if(PhpElementsUtil.isMethodReferenceInstanceOf(parentOfType, "\\PHPUnit\\Framework\\TestCase", "createPartialMock") ||
PhpElementsUtil.isMethodReferenceInstanceOf(parentOfType, "PHPUnit_Framework_TestCase", "createPartialMock")
) {
if (PhpUnitPluginUtil.isCreatePartialMockMethod(parentOfType)) {
PsiElement originalClassName = parentOfType.getParameter("originalClassName", 0);

if (originalClassName == null) {
Expand All @@ -103,13 +99,4 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar psiReferen
);
}
}

private static @NotNull Capture<StringLiteralExpression> getArrayParameterPattern() {
return PlatformPatterns.psiElement(StringLiteralExpression.class)
.withParent(PlatformPatterns.psiElement(PhpPsiElement.class)
.withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class)
.withParent(PlatformPatterns.psiElement(ParameterList.class)
.withParent(MethodReference.class))));

}
}
13 changes: 13 additions & 0 deletions src/main/java/de/espend/idea/php/phpunit/utils/PatternUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,17 @@ private static <T extends PsiElement> PsiElementPattern.Capture<T> createPattern
}
return PlatformPatterns.psiElement(baseClass).withParent(currentCapture);
}


/**
* "$this->createPartialMock(Foo::class, ['foobar']);"
*/
public static @NotNull PsiElementPattern.Capture<StringLiteralExpression> getArrayParameterPattern() {
return PlatformPatterns.psiElement(StringLiteralExpression.class)
.withParent(PlatformPatterns.psiElement(PhpPsiElement.class)
.withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class)
.withParent(PlatformPatterns.psiElement(ParameterList.class)
.withParent(MethodReference.class))));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,8 @@ public static Collection<LookupElement> getMockableMethods(@NotNull Project proj
return elements;
}

public static boolean isCreatePartialMockMethod(@NotNull MethodReference parentOfType) {
return PhpElementsUtil.isMethodReferenceInstanceOf(parentOfType, "\\PHPUnit\\Framework\\TestCase", "createPartialMock")
|| PhpElementsUtil.isMethodReferenceInstanceOf(parentOfType, "PHPUnit_Framework_TestCase", "createPartialMock");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public class CreateMockMethodReferenceProcessor implements ChainVisitorUtil.Chai

@Override
public boolean process(@NotNull MethodReference methodReference) {
if(PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "\\PHPUnit\\Framework\\TestCase", "createMock") ||
PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "PHPUnit_Framework_TestCase", "createMock")
) {
boolean isMockShortcutMethod = PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "\\PHPUnit\\Framework\\TestCase", "createMock")
|| PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "PHPUnit_Framework_TestCase", "createMock")
|| PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "\\PHPUnit\\Framework\\TestCase", "createPartialMock")
|| PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "PHPUnit_Framework_TestCase", "createPartialMock");

if (isMockShortcutMethod) {
PsiElement[] parameters = methodReference.getParameters();

if(parameters.length > 0) {
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@
displayName="Replace legacy Mockery syntax" groupName="Mockery" enabledByDefault="true" level="WARNING"
implementationClass="de.espend.idea.php.phpunit.inspection.ReplaceLegacyMockeryInspection"/>

<localInspection language="PHP" groupPath="PHP" shortName="DeprecatedMockedMethodInspection"
displayName="Mocked method is deprecated" groupName="PhpUnit" enabledByDefault="true" level="WARNING"
implementationClass="de.espend.idea.php.phpunit.inspection.DeprecatedMockedMethodInspection"/>
</extensions>

<application-components>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<body>
Reports the usages of deprecated entities.
<!-- tooltip end -->
In most cases, such usages should be removed or replaced with other constructs.
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import com.intellij.codeInsight.daemon.LineMarkerProviders;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.IntentionManager;
import com.intellij.codeInspection.*;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.ElementPattern;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
Expand Down Expand Up @@ -155,6 +158,55 @@ public void assertReferencesMatch(LanguageFileType languageFileType, String conf
fail(String.format("Failed pattern matches element of '%d' elements", parent.getReferences().length));
}

public void assertLocalInspectionContains(String filename, String content, String contains) {
Set<String> matches = new HashSet<String>();

Pair<List<ProblemDescriptor>, Integer> localInspectionsAtCaret = getLocalInspectionsAtCaret(filename, content);
for (ProblemDescriptor result : localInspectionsAtCaret.getFirst()) {
TextRange textRange = result.getPsiElement().getTextRange();
if (textRange.contains(localInspectionsAtCaret.getSecond()) && result.toString().equals(contains)) {
return;
}

matches.add(result.toString());
}

fail(String.format("Fail matches '%s' with one of %s", contains, matches));
}

private Pair<List<ProblemDescriptor>, Integer> getLocalInspectionsAtCaret(@NotNull String filename, @NotNull String content) {

PsiElement psiFile = myFixture.configureByText(filename, content);

int caretOffset = myFixture.getCaretOffset();
if(caretOffset <= 0) {
fail("Please provide <caret> tag");
}

ProblemsHolder problemsHolder = new ProblemsHolder(InspectionManager.getInstance(getProject()), psiFile.getContainingFile(), false);

for (LocalInspectionEP localInspectionEP : LocalInspectionEP.LOCAL_INSPECTION.getExtensions()) {
Object object = localInspectionEP.getInstance();
if(!(object instanceof LocalInspectionTool)) {
continue;
}

final PsiElementVisitor psiElementVisitor = ((LocalInspectionTool) object).buildVisitor(problemsHolder, false);

psiFile.acceptChildren(new PsiRecursiveElementVisitor() {
@Override
public void visitElement(@NotNull PsiElement element) {
psiElementVisitor.visitElement(element);
super.visitElement(element);
}
});

psiElementVisitor.visitFile(psiFile.getContainingFile());
}

return Pair.create(problemsHolder.getResults(), caretOffset);
}

public void assertMethodContainsTypes(@NotNull LanguageFileType languageFileType, @NotNull String configureByText, @NotNull String... types) {
myFixture.configureByText(languageFileType, configureByText);
PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package de.espend.idea.php.phpunit.inspection;

import de.espend.idea.php.phpunit.PhpUnitLightCodeInsightFixtureTestCase;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
* @see DeprecatedMockedMethodInspection
*/
public class DeprecatedMockedMethodInspectionTest extends PhpUnitLightCodeInsightFixtureTestCase {
public void setUp() throws Exception {
super.setUp();
myFixture.copyFileToProject("DeprecatedMockedMethodInspection.php");
}

@Override
public String getTestDataPath() {
return "src/test/java/de/espend/idea/php/phpunit/inspection/fixtures";
}

public void testDeprecationForMockedMethodViaCreatePartialMock() {
assertLocalInspectionContains("test.php", "<?php\n" +
"class Foo extends \\PHPUnit\\Framework\\TestCase\n" +
"{\n" +
" public function foobar()\n" +
" {\n" +
" $this->createPartialMock(\\Foo\\Bar::class, ['getFoobarD<caret>eprecated']);\n" +
" }\n" +
"}",
"Method 'getFoobarDeprecated' is deprecated"
);
}

public void testDeprecationForMockedMethodViaCreatePartialMock2() {
assertLocalInspectionContains("test.php", "<?php\n" +
"class Foo extends \\PHPUnit\\Framework\\TestCase\n" +
"{\n" +
" public function foobar()\n" +
" {\n" +
" $foo = $this->createPartialMock(\\Foo\\Bar::class, ['getFoobar']);\n" +
" $foo->method('getFoobarD<caret>eprecated');\n" +
" }\n" +
"}",
"Method 'getFoobarDeprecated' is deprecated"
);
}

public void testDeprecationForMockedMethodViaCreateMock() {
assertLocalInspectionContains("test2.php", "<?php\n" +
"class Foo extends \\PHPUnit\\Framework\\TestCase\n" +
"{\n" +
" public function foobar()\n" +
" {\n" +
" $foo = $this->createMock(\\Foo\\Bar::class);\n" +
" $foo->method('getFoobarD<caret>eprecated');\n" +
" }\n" +
"}",
"Method 'getFoobarDeprecated' is deprecated"
);
}
}
Loading

0 comments on commit d30152a

Please sign in to comment.