diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4e3424 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +out/ +/.idea +/.idea/misc.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..41f42a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +Phake Mocking Hints - PhpStorm Plugin + +Copyright (c) 2015, Mike Lively +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + Neither the name of Mike Lively nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/META-INF/plugin.xml b/META-INF/plugin.xml new file mode 100644 index 0000000..123a708 --- /dev/null +++ b/META-INF/plugin.xml @@ -0,0 +1,46 @@ + + com.digitalsandwich.phake + Phake Mocking Hints + 0.1 + Mike Lively + + + + + + + + + + + + + com.jetbrains.php + com.intellij.modules.platform + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PhakeMockPlugin.iml b/PhakeMockPlugin.iml new file mode 100644 index 0000000..fc19f7d --- /dev/null +++ b/PhakeMockPlugin.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/com/digitalsandwich/phake/PhakeCompletionContributor.java b/src/com/digitalsandwich/phake/PhakeCompletionContributor.java new file mode 100644 index 0000000..ad1866f --- /dev/null +++ b/src/com/digitalsandwich/phake/PhakeCompletionContributor.java @@ -0,0 +1,64 @@ +package com.digitalsandwich.phake; + +import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.patterns.PlatformPatterns; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ProcessingContext; +import com.jetbrains.php.PhpIcons; +import com.jetbrains.php.PhpIndex; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import org.jetbrains.annotations.NotNull; +import java.util.Collection; + +/** + * Handles code completion of class names as strings for mock() partialMock() and partMock() + */ +public class PhakeCompletionContributor extends CompletionContributor +{ + public PhakeCompletionContributor() + { + extend(CompletionType.BASIC, PlatformPatterns.psiElement(), new CompletionProvider() { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + MethodReference method = PsiTreeUtil.getContextOfType(completionParameters.getOriginalPosition(), MethodReference.class, true); + + if (method == null) + { + return; + } + + PsiElement[] parameters = method.getParameters(); + if (parameters.length < 1 || !(parameters[0] instanceof StringLiteralExpression)) + { + return; + } + + + if (method.getSignature().equals("#M#C\\Phake.mock") || method.getSignature().equals("#M#C\\Phake.partMock") || method.getSignature().equals("#M#C\\Phake.partialMock")) + { + PhpIndex phpIndex = PhpIndex.getInstance(method.getProject()); + Collection classNames = phpIndex.getAllClassNames(null); + for (String className : classNames) + { + LookupElementBuilder lookupElement = LookupElementBuilder.create(className) + .withTypeText(className) + .withIcon(PhpIcons.CLASS_ICON); + completionResultSet.addElement(lookupElement); + } + + Collection interfaceNames = phpIndex.getAllInterfaceNames(); + for (String interfaceName : interfaceNames) + { + LookupElementBuilder lookupElement = LookupElementBuilder.create(interfaceName) + .withTypeText(interfaceName) + .withIcon(PhpIcons.INTERFACE_ICON); + completionResultSet.addElement(lookupElement); + } + } + } + }); + } +} diff --git a/src/com/digitalsandwich/phake/PhakeMockTypeProvider.java b/src/com/digitalsandwich/phake/PhakeMockTypeProvider.java new file mode 100644 index 0000000..f60f2eb --- /dev/null +++ b/src/com/digitalsandwich/phake/PhakeMockTypeProvider.java @@ -0,0 +1,160 @@ +package com.digitalsandwich.phake; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.php.PhpIndex; +import com.jetbrains.php.lang.psi.elements.*; +import com.jetbrains.php.lang.psi.resolve.types.PhpType; +import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider2; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Handles the type translation + */ +public class PhakeMockTypeProvider implements PhpTypeProvider2 +{ + + public static final String CALLTYPE_MOCK = "<01>"; + public static final String CALLTYPE_VERIFICATION = "<02>"; + public static final String CALLTYPE_STUB = "<03>"; + public static final String CALLTYPE_STUBBED_METHOD = "<04>"; + + @Override + public char getKey() { + return '\u2002'; + } + + @Nullable + @Override + public String getType(PsiElement psiElement) { + if (psiElement instanceof MethodReference) + { + MethodReference methodReference = (MethodReference) psiElement; + String signature = methodReference.getSignature(); + if (isMockCall(signature)) + { + PsiElement[] parameters = methodReference.getParameters(); + + if (parameters.length > 0) + { + PsiElement parameter = parameters[0]; + if (parameter instanceof StringLiteralExpression) + { + String phpClassName = ((StringLiteralExpression)parameter).getContents(); + + if (StringUtil.isNotEmpty(phpClassName)) + { + return CALLTYPE_MOCK + signature + "~" + phpClassName; + } + } + } + } + + else if (isVerifyCall(signature)) + { + int parameterPosition = 0; + String typeList = passThruMethodParameterType(methodReference, parameterPosition); + if (StringUtil.isNotEmpty(typeList)) + { + return CALLTYPE_VERIFICATION + typeList; + } + } + + else if (isWhenCall(signature)) + { + int parameterPosition = 0; + String typeList = passThruMethodParameterType(methodReference, parameterPosition); + if (StringUtil.isNotEmpty(typeList)) + { + return CALLTYPE_STUB + typeList; + } + } + else if (signature.startsWith("#M#") && signature.contains(CALLTYPE_STUB)) + { + MethodReference previousMethodInChain = PsiTreeUtil.findChildOfType(psiElement, MethodReference.class); + + if (previousMethodInChain != null && (isWhenCall(previousMethodInChain.getSignature()))) + { + return CALLTYPE_STUBBED_METHOD; + } + } + else if (signature.startsWith("#M#") && signature.contains(CALLTYPE_STUBBED_METHOD)) + { + return CALLTYPE_STUBBED_METHOD; + } + } + return null; + } + + @Nullable + private String passThruMethodParameterType(MethodReference methodReference, int parameterPosition) { + String typeList = null; + PsiElement[] parameters = methodReference.getParameters(); + if (parameters.length > parameterPosition) + { + PsiElement parameter = parameters[parameterPosition]; + if (parameter instanceof Variable) + { + PhpType type = ((Variable) parameter).getType(); + typeList = StringUtil.join(type.getTypes(), "|"); + } + } + return typeList; + } + + @Override + public Collection getBySignature(String s, Project project) { + + PhpIndex phpIndex = PhpIndex.getInstance(project); + Collection signedClasses = new ArrayList(); + if (s.substring(0, 4).equals(CALLTYPE_MOCK)) + { + int separator = s.indexOf("~"); + String phakeSignature = s.substring(4, separator); + String className = s.substring(separator + 1); + + PhpClass phpClass = phpIndex.getClassByName(className); + signedClasses.addAll(phpIndex.getBySignature(phakeSignature)); + if (phpClass != null) + { + signedClasses.add(phpClass); + } + else + { + signedClasses.addAll(phpIndex.getInterfacesByName(className)); + } + } + else if (s.substring(0, 4).equals(CALLTYPE_VERIFICATION) || s.substring(0, 4).equals(CALLTYPE_STUB)) + { + for (String signature : StringUtil.split(s.substring(4),"|")) + { + Collection phpNamedElements = phpIndex.getBySignature(signature); + signedClasses.addAll(phpNamedElements); + } + } + else if (s.substring(0, 4).equals(CALLTYPE_STUBBED_METHOD)) + { + PhpClass answerBinder = phpIndex.getClassByName("Phake_Proxies_AnswerBinderProxy"); + signedClasses.add(answerBinder); + } + + return signedClasses.size() == 0 ? null : signedClasses; + } + + private boolean isWhenCall(String signature) { + return signature.equals("#M#C\\Phake.when") || signature.equals("#M#C\\Phake.whenStatic"); + } + + private boolean isVerifyCall(String signature) { + return signature.equals("#M#C\\Phake.verify") || signature.equals("#M#C\\Phake.verifyStatic"); + } + + private boolean isMockCall(String signature) { + return signature.equals("#M#C\\Phake.mock") || signature.equals("#M#C\\Phake.partialMock") || signature.equals("#M#C\\Phake.partMock"); + } +}