diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 266ca15f..4ff45fad 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -1,7 +1,7 @@ com.yii2support Yii2 Support - 0.2.2 + 0.2.10.9 NVlad Features ]]> -
  • Fix show autocompletion popup
  • +
  • Update path to View file on file move
  • +
  • Fixed replace View path on file rename
  • +
  • Fix mark as error view paths started with "//" or "@" (references to files is not work)
  • ]]>
    @@ -33,8 +45,27 @@ com.intellij.modules.platform - - + + + + + + + + + diff --git a/resources/inspectionDescriptions/MissedViewInspection.html b/resources/inspectionDescriptions/MissedViewInspection.html new file mode 100644 index 00000000..7758d890 --- /dev/null +++ b/resources/inspectionDescriptions/MissedViewInspection.html @@ -0,0 +1,9 @@ + + +Check for View file existing + +

    + - QuickFix - create View file +

    + + \ No newline at end of file diff --git a/resources/inspectionDescriptions/RequireParameterInspection.html b/resources/inspectionDescriptions/RequireParameterInspection.html new file mode 100644 index 00000000..fa11d05b --- /dev/null +++ b/resources/inspectionDescriptions/RequireParameterInspection.html @@ -0,0 +1,5 @@ + + +View require parameters + + diff --git a/resources/inspectionDescriptions/UnusedParameterInspection.html b/resources/inspectionDescriptions/UnusedParameterInspection.html new file mode 100644 index 00000000..c881de1d --- /dev/null +++ b/resources/inspectionDescriptions/UnusedParameterInspection.html @@ -0,0 +1,5 @@ + + +Detecting unused View parameters. + + diff --git a/src/com/yii2support/common/PhpUtil.java b/src/com/yii2support/common/PhpUtil.java new file mode 100644 index 00000000..b2633442 --- /dev/null +++ b/src/com/yii2support/common/PhpUtil.java @@ -0,0 +1,29 @@ +package com.yii2support.common; + +import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression; +import com.jetbrains.php.lang.psi.elements.ArrayHashElement; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Created by NVlad on 23.01.2017. + */ +public class PhpUtil { + @NotNull + public static Collection getArrayKeys(ArrayCreationExpression array) { + final HashSet result = new HashSet<>(); + + Iterable items = array.getHashElements(); + + for (ArrayHashElement item : items) { + if (item.getKey() != null && item.getKey() instanceof StringLiteralExpression) { + result.add(((StringLiteralExpression) item.getKey()).getContents()); + } + } + + return result; + } +} diff --git a/src/com/yii2support/common/PsiUtil.java b/src/com/yii2support/common/PsiUtil.java new file mode 100644 index 00000000..a814d2fc --- /dev/null +++ b/src/com/yii2support/common/PsiUtil.java @@ -0,0 +1,55 @@ +package com.yii2support.common; + +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression; + +/** + * Created by NVlad on 17.01.2017. + */ +public class PsiUtil { + public static void deleteArrayElement(PsiElement element) { + PsiElement next = element.getNextSibling(); + String endArray = ((ArrayCreationExpression) element.getParent()).isShortSyntax() ? "]" : ")"; + + if (next instanceof PsiWhiteSpace && next.getNextSibling().getText() != null) { + if (next.getNextSibling().getText().equals(endArray)) { + next = next.getNextSibling(); + } + } + if (next.getText().equals(endArray)) { + Boolean deleteComma = false; + if (element.getPrevSibling() instanceof PsiWhiteSpace) { + deleteComma = !element.getPrevSibling().getText().contains("\n"); + element.getPrevSibling().delete(); + } + if (deleteComma && element.getPrevSibling().getText().equals(",")) { + element.getPrevSibling().delete(); + } + } + if (next.getText().equals(",")) { + if (next.getNextSibling() instanceof PsiWhiteSpace) { + next.getNextSibling().delete(); + } + next.delete(); + } + element.delete(); + } + + public static void deleteFunctionParam(PsiElement element) { + PsiElement next = element.getNextSibling(); + if (next != null && next.getText().equals(",")) { + next.delete(); + } else { + PsiElement prev = element.getPrevSibling(); + if (prev != null && prev instanceof PsiWhiteSpace) { + prev.delete(); + prev = element.getPrevSibling(); + } + if (prev != null && prev.getText().equals(",")) { + prev.delete(); + } + } + element.delete(); + } +} diff --git a/src/com/yii2support/i18n/CategoryLookupElement.java b/src/com/yii2support/i18n/CategoryLookupElement.java index 30d2de37..9dcf8436 100644 --- a/src/com/yii2support/i18n/CategoryLookupElement.java +++ b/src/com/yii2support/i18n/CategoryLookupElement.java @@ -9,8 +9,8 @@ /** * Created by NVlad on 06.01.2017. */ -public class CategoryLookupElement extends LookupElement { - private PsiElement myCategory; +class CategoryLookupElement extends LookupElement { + final private PsiElement myCategory; CategoryLookupElement(PsiElement category) { myCategory = category; diff --git a/src/com/yii2support/i18n/CompletionContributor.java b/src/com/yii2support/i18n/CompletionContributor.java index 1c4a3759..f0aa9c5f 100644 --- a/src/com/yii2support/i18n/CompletionContributor.java +++ b/src/com/yii2support/i18n/CompletionContributor.java @@ -5,6 +5,8 @@ import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.php.lang.psi.elements.ClassReference; import com.jetbrains.php.lang.psi.elements.MethodReference; import com.jetbrains.php.lang.psi.elements.ParameterList; import com.yii2support.common.Patterns; @@ -20,12 +22,19 @@ public CompletionContributor() { @Override public boolean invokeAutoPopup(@NotNull PsiElement position, char typeChar) { - if (typeChar == '\'' || typeChar == '"') { - if (position instanceof LeafPsiElement && (position.getText().equals("$category") || position.getText().equals("$message"))) { - return true; + MethodReference reference = PsiTreeUtil.getParentOfType(position, MethodReference.class); + if (reference != null && reference.getName() != null && reference.getName().equals("t")) { + ClassReference classReference = (ClassReference) reference.getClassReference(); + if (classReference == null || classReference.getName() == null || !classReference.getName().equals("Yii")) { + return false; } - if (position.getNextSibling() instanceof ParameterList || position.getParent() instanceof MethodReference) { - return true; + if (typeChar == '\'' || typeChar == '"') { + if (position instanceof LeafPsiElement && (position.getText().equals("$category") || position.getText().equals("$message"))) { + return true; + } + if (position.getNextSibling() instanceof ParameterList) { + return true; + } } } diff --git a/src/com/yii2support/i18n/CompletionProvider.java b/src/com/yii2support/i18n/CompletionProvider.java index e13aee3b..922aae14 100644 --- a/src/com/yii2support/i18n/CompletionProvider.java +++ b/src/com/yii2support/i18n/CompletionProvider.java @@ -10,7 +10,7 @@ /** * Created by NVlad on 06.01.2017. */ -public class CompletionProvider extends com.intellij.codeInsight.completion.CompletionProvider { +class CompletionProvider extends com.intellij.codeInsight.completion.CompletionProvider { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet result) { PhpPsiElement psiElement = (PhpPsiElement) parameters.getPosition().getParent(); diff --git a/src/com/yii2support/i18n/MessageLookupElement.java b/src/com/yii2support/i18n/MessageLookupElement.java index 9c147ce2..a7a390a9 100644 --- a/src/com/yii2support/i18n/MessageLookupElement.java +++ b/src/com/yii2support/i18n/MessageLookupElement.java @@ -15,9 +15,9 @@ /** * Created by NVlad on 06.01.2017. */ -public class MessageLookupElement extends LookupElement { - private PhpPsiElement myElement; - private ArrayHashElement myMessage; +class MessageLookupElement extends LookupElement { + final private PhpPsiElement myElement; + final private ArrayHashElement myMessage; MessageLookupElement(PhpPsiElement element, ArrayHashElement message) { myElement = element; diff --git a/src/com/yii2support/views/CompletionProvider.java b/src/com/yii2support/views/CompletionProvider.java deleted file mode 100644 index 3fd708de..00000000 --- a/src/com/yii2support/views/CompletionProvider.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.yii2support.views; - -import com.intellij.codeInsight.completion.CompletionParameters; -import com.intellij.codeInsight.completion.CompletionResultSet; -import com.intellij.codeInsight.template.macro.SplitWordsMacro; -import com.intellij.psi.PsiDirectory; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.util.ArrayUtil; -import com.intellij.util.ProcessingContext; -import com.jetbrains.php.lang.psi.elements.MethodReference; -import org.jetbrains.annotations.NotNull; - -/** - * Created by NVlad on 27.12.2016. - */ -public class CompletionProvider extends com.intellij.codeInsight.completion.CompletionProvider { - @Override - protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { - PsiElement psiElement = completionParameters.getPosition(); - MethodReference methodReference = (MethodReference) psiElement.getParent().getParent().getParent(); - String methodName = methodReference.getName(); - - if (ArrayUtil.indexOf(methodReference.getParameters(), psiElement.getParent()) == 0) { - if (methodName != null) { - if (completionResultSet.getPrefixMatcher().getPrefix().contains("/")) { - String prefix = completionResultSet.getPrefixMatcher().getPrefix(); - prefix = prefix.substring(prefix.lastIndexOf("/") + 1); - completionResultSet = completionResultSet.withPrefixMatcher(prefix); - } - PsiDirectory viewsPath = getViewsPsiDirectory(completionParameters.getOriginalFile(), psiElement); - - if (viewsPath != null) { - for (PsiDirectory psiDirectory : viewsPath.getSubdirectories()) { - completionResultSet.addElement(new DirectoryLookupElement(psiDirectory)); - } - - for (PsiFile psiFile : viewsPath.getFiles()) { - completionResultSet.addElement(new ViewLookupElement(psiFile)); - } - } - } - } - } - - private PsiDirectory getViewsPsiDirectory(PsiFile psiFile, PsiElement psiElement) { - String fileName = psiFile.getName().substring(0, psiFile.getName().lastIndexOf(".")); - PsiDirectory psiDirectory = psiFile.getContainingDirectory(); - - if (fileName.endsWith("Controller")) { - psiDirectory = psiFile.getContainingDirectory().getParentDirectory(); - if (psiDirectory != null) { - psiDirectory = psiDirectory.findSubdirectory("views"); - - if (psiDirectory != null) { - String container = fileName.substring(0, fileName.length() - 10); - container = new SplitWordsMacro.LowercaseAndDash().convertString(container); - - psiDirectory = psiDirectory.findSubdirectory(container); - } - } - } - - String enteredText = psiElement.getText(); - int iir = enteredText.indexOf("IntellijIdeaRulezzz "); - if (iir != -1) { - enteredText = enteredText.substring(0, iir); - } - String enteredPath = enteredText; - if (enteredText.startsWith("/")) { - while (psiDirectory != null && !psiDirectory.getName().equals("views")) { - psiDirectory = psiDirectory.getParentDirectory(); - } - enteredPath = enteredPath.substring(1); - } - - if (!enteredPath.endsWith("/") && enteredPath.contains("/")) { - enteredPath = enteredPath.substring(0, enteredPath.lastIndexOf("/") + 1); - if (enteredPath.length() == 1) { - enteredPath = ""; - } - } - - if (enteredPath.endsWith("/")) { - String directory; - while (!enteredPath.equals("")) { - directory = enteredPath.substring(0, enteredPath.indexOf("/")); - enteredPath = enteredPath.substring(directory.length() + 1); - if (psiDirectory != null) { - psiDirectory = psiDirectory.findSubdirectory(directory); - } - } - } - - return psiDirectory; - } -} diff --git a/src/com/yii2support/views/PsiReference.java b/src/com/yii2support/views/PsiReference.java deleted file mode 100644 index ca1ce751..00000000 --- a/src/com/yii2support/views/PsiReference.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.yii2support.views; - -import com.intellij.codeInsight.template.macro.SplitWordsMacro; -import com.intellij.psi.PsiDirectory; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiReferenceBase; -import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Created by NVlad on 02.01.2017. - */ -public class PsiReference extends PsiReferenceBase { - private PsiElement myTarget; - - PsiReference(@NotNull PsiElement element) { - super(element); - } - - @Nullable - @Override - public PsiElement resolve() { - if (myTarget == null) { - PsiFile psiFile = getViewPsiFile(myElement); - if (psiFile != null) { - myTarget = psiFile.getOriginalElement(); - } - } - - return myTarget; - } - - @NotNull - @Override - public Object[] getVariants() { - return new Object[0]; - } - - private PsiFile getViewPsiFile(PsiElement psiElement) { - PsiFile psiFile = psiElement.getContainingFile(); - StringLiteralExpression expression = (StringLiteralExpression) psiElement; - String filename = expression.getContents(); - if (filename.contains("/")) { - filename = filename.substring(filename.lastIndexOf("/") + 1); - } - if (!filename.contains(".")) { - filename = filename.concat(".php"); - } - - PsiDirectory directory = getViewsPsiDirectory(psiFile, psiElement); - - if (directory == null) { - return null; - } - - return directory.findFile(filename); - } - - private PsiDirectory getViewsPsiDirectory(PsiFile psiFile, PsiElement psiElement) { - String fileName = psiFile.getName().substring(0, psiFile.getName().lastIndexOf(".")); - PsiDirectory psiDirectory = psiFile.getContainingDirectory(); - - if (fileName.endsWith("Controller")) { - psiDirectory = psiFile.getContainingDirectory().getParentDirectory(); - if (psiDirectory != null) { - psiDirectory = psiDirectory.findSubdirectory("views"); - - if (psiDirectory != null) { - String container = fileName.substring(0, fileName.length() - 10); - container = new SplitWordsMacro.LowercaseAndDash().convertString(container); - - psiDirectory = psiDirectory.findSubdirectory(container); - } - } - } - - String enteredText = ((StringLiteralExpression) psiElement).getContents(); - String enteredPath = enteredText; - if (enteredText.startsWith("/")) { - while (psiDirectory != null && !psiDirectory.getName().equals("views")) { - psiDirectory = psiDirectory.getParentDirectory(); - } - enteredPath = enteredPath.substring(1); - } - - if (!enteredPath.endsWith("/") && enteredPath.contains("/")) { - enteredPath = enteredPath.substring(0, enteredPath.lastIndexOf("/") + 1); - if (enteredPath.length() == 1) { - enteredPath = ""; - } - } - - if (enteredPath.endsWith("/")) { - String directory; - while (!enteredPath.equals("")) { - directory = enteredPath.substring(0, enteredPath.indexOf("/")); - enteredPath = enteredPath.substring(directory.length() + 1); - if (psiDirectory != null) { - psiDirectory = psiDirectory.findSubdirectory(directory); - } - } - } - - return psiDirectory; - } -} diff --git a/src/com/yii2support/views/ViewLookupElement.java b/src/com/yii2support/views/ViewLookupElement.java deleted file mode 100644 index 64aa161d..00000000 --- a/src/com/yii2support/views/ViewLookupElement.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.yii2support.views; - -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementPresentation; -import com.intellij.psi.PsiFile; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; - -/** - * Created by NVlad on 28.12.2016. - */ -public class ViewLookupElement extends LookupElement { - private String name; - private String tail; - private Icon icon; - - ViewLookupElement(PsiFile psiFile) { - String filename = psiFile.getName().substring(0, psiFile.getName().lastIndexOf(".")); - if (filename.contains(".")) { - name = psiFile.getName(); - } else { - name = filename; - tail = psiFile.getName().substring(filename.length()); - } - icon = psiFile.getIcon(0); - } - - @NotNull - @Override - public String getLookupString() { - return name; - } - - @Override - public void renderElement(LookupElementPresentation presentation) { - super.renderElement(presentation); - - presentation.setIcon(icon); - presentation.setItemText(name); - presentation.setItemTextBold(true); - if (tail != null) { - presentation.setTailText(tail, true); - } - presentation.setTypeText("View"); - presentation.setTypeGrayed(true); - } -} diff --git a/src/com/yii2support/views/ViewsUtil.java b/src/com/yii2support/views/ViewsUtil.java new file mode 100644 index 00000000..478bd273 --- /dev/null +++ b/src/com/yii2support/views/ViewsUtil.java @@ -0,0 +1,294 @@ +package com.yii2support.views; + +import com.intellij.codeInsight.template.macro.SplitWordsMacro; +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.php.lang.parser.PhpElementTypes; +import com.jetbrains.php.lang.psi.PhpFile; +import com.jetbrains.php.lang.psi.elements.*; +import gnu.trove.THashSet; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Created by NVlad on 15.01.2017. + */ +public class ViewsUtil { + public static final Key RENDER_VIEW = Key.create("com.yii2support.views.render.view"); + public static final Key RENDER_VIEW_FILE = Key.create("com.yii2support.views.viewFile"); + private static final Key RENDER_VIEW_PATH = Key.create("views.viewPath"); + private static final Key VIEW_FILE_MODIFIED = Key.create("com.yii2support.views.viewFileModified"); + private static final Key> VIEW_VARIABLES = Key.create("com.yii2support.views.viewVariables"); + private static final Key VIEWS_DIRECTORY = Key.create("views.directory"); + private static final Key VIEWS_DIRECTORY_MODIFIED = Key.create("views.directory.modified"); + private static final Key VIEWS_CONTEXT_DIRECTORY = Key.create("views.context.directory"); + + private static final Set ignoredVariables = getIgnoredVariables(); + + public static final String[] renderMethods = {"render", "renderAjax", "renderPartial"}; + + private static Set getIgnoredVariables() { + final Set set = new THashSet<>(Arrays.asList("this", "_file_", "_params_")); + set.addAll(Variable.SUPERGLOBALS); + return set; + } + + @NotNull + private static ArrayList getPhpViewVariables(PsiFile psiFile) { + final ArrayList result = new ArrayList<>(); + final HashSet allVariables = new HashSet<>(); + final HashSet declaredVariables = new HashSet<>(); + final Collection viewVariables = PsiTreeUtil.findChildrenOfType(psiFile, Variable.class); + + for (FunctionReference reference : PsiTreeUtil.findChildrenOfType(psiFile, FunctionReference.class)) { + if (reference.getNode().getElementType() == PhpElementTypes.FUNCTION_CALL && psiFile.getUseScope().equals(reference.getUseScope())) { + if (reference.getName() != null && reference.getName().equals("compact")) { + for (PsiElement element : reference.getParameters()) { + if (element instanceof StringLiteralExpression) { + allVariables.add(((StringLiteralExpression) element).getContents()); + } + } + } + } + } + + for (Variable variable : viewVariables) { + String variableName = variable.getName(); + if (variable.isDeclaration()) { + declaredVariables.add(variableName); + } else { + if (!ignoredVariables.contains(variableName)) { + if (psiFile.getUseScope().equals(variable.getUseScope())) { + if (variable.getName().equals("") && variable.getParent() instanceof StringLiteralExpression) { + Variable inlineVariable = PsiTreeUtil.findChildOfType(variable, Variable.class); + if (inlineVariable != null) { + allVariables.add(inlineVariable.getName()); + } + } else { + allVariables.add(variableName); + } + } + } + } + } + + for (String variable : allVariables) { + if (!declaredVariables.contains(variable)) { + result.add(variable); + } + } + + return result; + } + + @NotNull + public static ArrayList getViewVariables(PsiFile psiFile) { + ArrayList result = null; + + Long viewModified = psiFile.getUserData(VIEW_FILE_MODIFIED); + if (viewModified != null && psiFile.getModificationStamp() == viewModified) { + result = psiFile.getUserData(VIEW_VARIABLES); + } + + if (result == null) { + if (psiFile instanceof PhpFile) { + result = getPhpViewVariables(psiFile); + } + + if (result == null) { + result = new ArrayList<>(); + } + + psiFile.putUserData(VIEW_VARIABLES, result); + psiFile.putUserData(VIEW_FILE_MODIFIED, psiFile.getModificationStamp()); + } + + return new ArrayList<>(result); + } + + public static PsiFile getViewFile(PsiElement element) { + final MethodReference reference = PsiTreeUtil.getParentOfType(element, MethodReference.class); + if (reference == null) { + return null; + } + + final PsiElement[] parameters = reference.getParameters(); + if (parameters.length == 0 || !(parameters[0] instanceof StringLiteralExpression)) { + return null; + } + + String view = reference.getUserData(RENDER_VIEW); + if (!((StringLiteralExpression) parameters[0]).getContents().equals(view)) { + view = ((StringLiteralExpression) parameters[0]).getContents(); + reference.putUserData(RENDER_VIEW, view); + reference.putUserData(RENDER_VIEW_FILE, null); + reference.putUserData(RENDER_VIEW_PATH, null); + } + + PsiFile file = reference.getUserData(RENDER_VIEW_FILE); + if (file != null && file.isValid()) { + if (!file.getVirtualFile().getPath().equals(reference.getUserData(RENDER_VIEW_PATH))) { + reference.putUserData(RENDER_VIEW_FILE, null); + reference.putUserData(RENDER_VIEW_PATH, null); + file = null; + } + } + + if (file == null || !file.isValid()) { + if (reference.getParameters()[0] instanceof StringLiteralExpression) { + PsiDirectory directory; + + String path = ((StringLiteralExpression) reference.getParameters()[0]).getContents(); + if (path.startsWith("/")) { + directory = ViewsUtil.getRootDirectory(element); + path = path.substring(1); + } else { + directory = ViewsUtil.getContextDirectory(element); + } + + String filename; + if (path.contains("/")) { + filename = path.substring(path.lastIndexOf('/') + 1); + path = path.substring(0, path.lastIndexOf('/') + 1); + } else { + filename = path; + path = ""; + } + + while (path.contains("/") && directory != null) { + final String dirName = path.substring(0, path.indexOf('/')); + directory = dirName.equals("..") ? directory.getParent() : directory.findSubdirectory(dirName); + path = path.substring(path.indexOf('/') + 1); + } + + if (directory == null) { + return null; + } + + if (filename.contains(".")) { + file = directory.findFile(filename); + } else { + file = directory.findFile(filename + ".php"); + if (file == null) { + file = directory.findFile(filename + ".twig"); + } + if (file == null) { + file = directory.findFile(filename + ".tpl"); + } + } + + if (file != null) { + reference.putUserData(RENDER_VIEW_FILE, file); + reference.putUserData(RENDER_VIEW_PATH, file.getVirtualFile().getPath()); + } + } + } + + return file; + } + + public static PsiDirectory getRootDirectory(PsiElement element) { + final PsiFile file = element.getContainingFile(); + + if (file.getUserData(VIEWS_DIRECTORY) != null) { + Long modified = file.getUserData(VIEWS_DIRECTORY_MODIFIED); + if (modified != null && modified == file.getModificationStamp()) { + return file.getUserData(VIEWS_DIRECTORY); + } + } + + return findDirectory(element); + } + + public static PsiDirectory getContextDirectory(PsiElement element) { + final PsiFile file = element.getContainingFile(); + + if (file.getUserData(VIEWS_CONTEXT_DIRECTORY) != null) { + Long modified = file.getUserData(VIEWS_DIRECTORY_MODIFIED); + if (modified != null && modified == file.getModificationStamp()) { + return file.getUserData(VIEWS_CONTEXT_DIRECTORY); + } + } + + findDirectory(element); + + return file.getUserData(VIEWS_CONTEXT_DIRECTORY); + } + + private static PsiDirectory findDirectory(PsiElement element) { + final PhpClass phpClass = PsiTreeUtil.getParentOfType(element, PhpClass.class); + final PsiFile file = element.getContainingFile(); + if (phpClass != null) { + return findClassDirectory(phpClass, file); + } + + PsiDirectory context = file.getOriginalFile().getContainingDirectory(); + PsiDirectory root = context.getParentDirectory(); + while (root != null && !root.getName().equals("views")) { + root = root.getParentDirectory(); + } + + file.putUserData(VIEWS_CONTEXT_DIRECTORY, context); + file.putUserData(VIEWS_DIRECTORY, root); + file.putUserData(VIEWS_DIRECTORY_MODIFIED, file.getModificationStamp()); + + return root; + } + + private static PsiDirectory findClassDirectory(PhpClass phpClass, PsiFile file) { + if (phpClass != null) { + Method getViewPath = phpClass.findMethodByName("getViewPath"); + PhpClass ownClass = phpClass.getSuperClass(); + PsiDirectory directory = file.getOriginalFile().getContainingDirectory(); + if (directory != null) { + while (ownClass != null) { + if (getViewPath != null) { + getViewPath = ownClass.findMethodByName("getViewPath"); + } + switch (ownClass.getFQN()) { + case "\\yii\\base\\Controller": + directory = directory.getParentDirectory(); + if (directory == null) { + return null; + } + if (getViewPath != null && Objects.equals(getViewPath.getContainingClass(), ownClass)) { + directory = directory.findSubdirectory("views"); + } + + if (directory != null) { + file.putUserData(VIEWS_DIRECTORY_MODIFIED, file.getModificationStamp()); + file.putUserData(VIEWS_DIRECTORY, directory); + + String controllerId = phpClass.getName(); + controllerId = controllerId.substring(0, controllerId.length() - 10); + controllerId = new SplitWordsMacro.LowercaseAndDash().convertString(controllerId); + + PsiDirectory context = directory.findSubdirectory(controllerId); + if (context != null) { + file.putUserData(VIEWS_CONTEXT_DIRECTORY, context); + } + } + + return directory; + case "\\yii\\base\\Widget": + directory = directory.findSubdirectory("views"); + if (directory != null) { + file.putUserData(VIEWS_DIRECTORY_MODIFIED, file.getModificationStamp()); + file.putUserData(VIEWS_DIRECTORY, directory); + file.putUserData(VIEWS_CONTEXT_DIRECTORY, directory); + } + + return directory; + } + ownClass = ownClass.getSuperClass(); + } + } + } + + return null; + } +} diff --git a/src/com/yii2support/views/CompletionContributor.java b/src/com/yii2support/views/completion/CompletionContributor.java similarity index 52% rename from src/com/yii2support/views/CompletionContributor.java rename to src/com/yii2support/views/completion/CompletionContributor.java index bf451083..3a3c8707 100644 --- a/src/com/yii2support/views/CompletionContributor.java +++ b/src/com/yii2support/views/completion/CompletionContributor.java @@ -1,12 +1,16 @@ -package com.yii2support.views; +package com.yii2support.views.completion; import com.intellij.codeInsight.completion.CompletionType; import com.intellij.patterns.ElementPattern; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.impl.source.tree.LeafPsiElement; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ArrayUtil; +import com.jetbrains.php.lang.psi.elements.MethodReference; import com.jetbrains.php.lang.psi.elements.ParameterList; import com.yii2support.common.Patterns; +import com.yii2support.views.ViewsUtil; import org.jetbrains.annotations.NotNull; /** @@ -19,12 +23,15 @@ public CompletionContributor() { @Override public boolean invokeAutoPopup(@NotNull PsiElement position, char typeChar) { - if (typeChar == '\'' || typeChar == '"') { - if (position instanceof LeafPsiElement && position.getText().equals("$view")) { - return true; - } - if (position.getNextSibling() instanceof ParameterList) { - return true; + MethodReference reference = PsiTreeUtil.getParentOfType(position, MethodReference.class); + if (reference != null && ArrayUtil.contains(reference.getName(), ViewsUtil.renderMethods)) { + if (typeChar == '\'' || typeChar == '"') { + if (position instanceof LeafPsiElement && position.getText().equals("$view")) { + return true; + } + if (position.getNextSibling() instanceof ParameterList) { + return true; + } } } @@ -32,8 +39,7 @@ public boolean invokeAutoPopup(@NotNull PsiElement position, char typeChar) { } private static ElementPattern ElementPattern() { - return PlatformPatterns.psiElement() - .withSuperParent(3, Patterns.methodWithName("render", "renderAjax", "renderPartial")); + .withSuperParent(3, Patterns.methodWithName(ViewsUtil.renderMethods)); } } diff --git a/src/com/yii2support/views/completion/CompletionProvider.java b/src/com/yii2support/views/completion/CompletionProvider.java new file mode 100644 index 00000000..08c4362f --- /dev/null +++ b/src/com/yii2support/views/completion/CompletionProvider.java @@ -0,0 +1,81 @@ +package com.yii2support.views.completion; + +import com.intellij.codeInsight.completion.CompletionParameters; +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ProcessingContext; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.NotNull; + +/** + * Created by NVlad on 27.12.2016. + */ +class CompletionProvider extends com.intellij.codeInsight.completion.CompletionProvider { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) { + final PsiElement psiElement = completionParameters.getPosition(); + final MethodReference method = PsiTreeUtil.getParentOfType(psiElement, MethodReference.class); + + if (method == null || method.getParameters().length == 0) { + return; + } + + PsiElement parameter = psiElement; + while (!(parameter.getParent() instanceof ParameterList)) { + parameter = parameter.getParent(); + } + if (!parameter.equals(method.getParameters()[0])) { + return; + } + + String path = getValue(method.getParameters()[0]); + PsiDirectory directory; + if (path.startsWith("/")) { + path = path.substring(1); + directory = ViewsUtil.getRootDirectory(psiElement); + } else { + directory = ViewsUtil.getContextDirectory(psiElement); + } + if (path.contains("/")) { + path = path.substring(0, path.lastIndexOf('/') + 1); + } + + while (path.contains("/") && directory != null) { + String subdirectory = path.substring(0, path.indexOf('/')); + path = path.substring(path.indexOf('/') + 1); + directory = subdirectory.equals("..") ? directory.getParent() : directory.findSubdirectory(subdirectory); + } + + if (directory != null) { + if (completionResultSet.getPrefixMatcher().getPrefix().contains("/")) { + String prefix = completionResultSet.getPrefixMatcher().getPrefix(); + prefix = prefix.substring(prefix.lastIndexOf("/") + 1); + completionResultSet = completionResultSet.withPrefixMatcher(prefix); + } + + for (PsiDirectory psiDirectory : directory.getSubdirectories()) { + completionResultSet.addElement(new DirectoryLookupElement(psiDirectory)); + } + + for (PsiFile psiFile : directory.getFiles()) { + completionResultSet.addElement(new ViewLookupElement(psiFile)); + } + } + } + + @NotNull + private String getValue(PsiElement expression) { + if (expression instanceof StringLiteralExpression) { + String value = ((StringLiteralExpression) expression).getContents(); + return value.substring(0, value.indexOf("IntellijIdeaRulezzz ")); + } + + return ""; + } +} diff --git a/src/com/yii2support/views/DirectoryLookupElement.java b/src/com/yii2support/views/completion/DirectoryLookupElement.java similarity index 83% rename from src/com/yii2support/views/DirectoryLookupElement.java rename to src/com/yii2support/views/completion/DirectoryLookupElement.java index 4bedd3d3..4166bf14 100644 --- a/src/com/yii2support/views/DirectoryLookupElement.java +++ b/src/com/yii2support/views/completion/DirectoryLookupElement.java @@ -1,4 +1,4 @@ -package com.yii2support.views; +package com.yii2support.views.completion; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; @@ -10,9 +10,9 @@ /** * Created by NVlad on 28.12.2016. */ -public class DirectoryLookupElement extends LookupElement { - private Icon icon; - private String name; +class DirectoryLookupElement extends LookupElement { + final private Icon icon; + final private String name; DirectoryLookupElement(PsiDirectory directory) { icon = directory.getIcon(0); diff --git a/src/com/yii2support/views/completion/ViewLookupElement.java b/src/com/yii2support/views/completion/ViewLookupElement.java new file mode 100644 index 00000000..a597ea84 --- /dev/null +++ b/src/com/yii2support/views/completion/ViewLookupElement.java @@ -0,0 +1,110 @@ +package com.yii2support.views.completion; + +import com.intellij.codeInsight.completion.InsertionContext; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; + +/** + * Created by NVlad on 28.12.2016. + */ +class ViewLookupElement extends LookupElement { + final private PsiFile myFile; + final private String myName; + final private String myTail; + + ViewLookupElement(PsiFile psiFile) { + myFile = psiFile; + VirtualFile file = psiFile.getVirtualFile(); + + if (file.getNameWithoutExtension().contains(".")) { + myName = file.getName(); + myTail = null; + } else { + myName = file.getNameWithoutExtension(); + myTail = "." + file.getExtension(); + } + } + + @NotNull + @Override + public String getLookupString() { + return myName; + } + + @Override + public void renderElement(LookupElementPresentation presentation) { + presentation.setIcon(myFile.getIcon(0)); + presentation.setItemText(myName); + presentation.setItemTextBold(true); + if (myTail != null) { + presentation.setTailText(myTail, true); + } + presentation.setTypeText("View"); + presentation.setTypeGrayed(true); + } + + @Override + public void handleInsert(InsertionContext context) { + PsiElement element = context.getFile().getViewProvider().findElementAt(context.getSelectionEndOffset()); + element = PsiTreeUtil.getParentOfType(element, StringLiteralExpression.class); + if (element == null) { + return; + } + + String value = ((StringLiteralExpression) element).getContents(); + if (value.contains("/")) { + value = value.substring(value.lastIndexOf('/') + 1); + } + if (!myName.equals(value)) { + String filename = myFile.getName(); + if (!filename.equals(value)) { + return; + } + } + + MethodReference reference = PsiTreeUtil.getParentOfType(element, MethodReference.class); + if (reference != null) { + reference.putUserData(ViewsUtil.RENDER_VIEW, ((StringLiteralExpression) element).getContents()); + reference.putUserData(ViewsUtil.RENDER_VIEW_FILE, myFile); + } + + ArrayList params = ViewsUtil.getViewVariables(myFile); + if (params.size() > 0) { + ParameterList parameterList = (ParameterList) element.getParent(); + if (parameterList.getParameters().length == 1) { + Project project = context.getProject(); + Template template = TemplateManager.getInstance(project).createTemplate("", ""); + template.addTextSegment(", ["); + boolean addComma = false; + for (String param : params) { + String variableName = "$" + param.toUpperCase() + "$"; + if (addComma) { + template.addTextSegment(", "); + } + template.addTextSegment("'" + param + "' => "); + template.addVariable(variableName, "", "\"$" + param + "\"", true); + template.addVariableSegment(variableName); + addComma = true; + } + template.addTextSegment("]"); + int offset = parameterList.getParameters()[0].getTextRange().getEndOffset(); + context.getEditor().getCaretModel().moveToOffset(offset); + TemplateManager.getInstance(project).startTemplate(context.getEditor(), template); + } + } + } +} diff --git a/src/com/yii2support/views/inspections/MissedViewInspection.java b/src/com/yii2support/views/inspections/MissedViewInspection.java new file mode 100644 index 00000000..dd4d0495 --- /dev/null +++ b/src/com/yii2support/views/inspections/MissedViewInspection.java @@ -0,0 +1,56 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.util.ArrayUtil; +import com.jetbrains.php.lang.inspections.PhpInspection; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.NotNull; + +/** + * Created by NVlad on 15.01.2017. + */ +final public class MissedViewInspection extends PhpInspection { + @NotNull + @Override + public String getShortName() { + return "MissedViewInspection"; + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder problemsHolder, boolean isOnTheFly) { + return new PhpElementVisitor() { + @Override + public void visitPhpMethodReference(MethodReference reference) { + if (ArrayUtil.contains(reference.getName(), ViewsUtil.renderMethods)) { + if (reference.getParameters().length > 0) { + PsiElement pathParameter = reference.getParameters()[0]; + if (pathParameter instanceof StringLiteralExpression) { + String path = ((StringLiteralExpression) pathParameter).getContents(); + if (path.startsWith("//") || path.startsWith("@")) { + return; + } + + PsiFile file = ViewsUtil.getViewFile(pathParameter); + if (file == null || !file.isValid()) { + final String errorViewNotFoundTemplate = "View file for \"%name%\" not found."; + final MissedViewLocalQuickFix quickFix = new MissedViewLocalQuickFix(path); + final String descriptionTemplate = errorViewNotFoundTemplate.replace("%name%", path); + final PsiElement stringPart = pathParameter.findElementAt(1); + if (stringPart != null) { + problemsHolder.registerProblem(stringPart, descriptionTemplate, quickFix); + } + } + } + } + } + } + }; + } +} diff --git a/src/com/yii2support/views/inspections/MissedViewLocalQuickFix.java b/src/com/yii2support/views/inspections/MissedViewLocalQuickFix.java new file mode 100644 index 00000000..eb5b18e4 --- /dev/null +++ b/src/com/yii2support/views/inspections/MissedViewLocalQuickFix.java @@ -0,0 +1,105 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.ide.fileTemplates.FileTemplate; +import com.intellij.ide.fileTemplates.FileTemplateManager; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.Properties; + +/** + * Created by NVlad on 15.01.2017. + */ +class MissedViewLocalQuickFix implements LocalQuickFix { + final private String myName; + + MissedViewLocalQuickFix(String name) { + myName = name; + } + + @Nls + @NotNull + @Override + public String getName() { + return "Create view for \"%name%\"".replace("%name%", myName); + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Create view"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + final PsiElement element = descriptor.getPsiElement().getParent(); + PsiDirectory directory; + String path = null; + + if (element instanceof StringLiteralExpression) { + path = ((StringLiteralExpression) element).getContents(); + } + + if (path == null) { + return; + } + + if (path.startsWith("/")) { + directory = ViewsUtil.getRootDirectory(element); + path = path.substring(1); + } else { + directory = ViewsUtil.getContextDirectory(element); + } + + if (directory == null) { + return; + } + + while (directory != null && path.contains("/")) { + final String subdirectory = path.substring(0, path.indexOf('/')); + path = path.substring(path.indexOf('/') + 1); + + directory = directory.findSubdirectory(subdirectory); + } + + if (directory != null) { + if (!path.contains(".")) { + path = path + ".php"; + } + + final PsiFile viewPsiFile = directory.createFile(path); + FileEditorManager.getInstance(project).openFile(viewPsiFile.getVirtualFile(), true); + + final FileTemplate[] templates = FileTemplateManager.getDefaultInstance().getTemplates(FileTemplateManager.DEFAULT_TEMPLATES_CATEGORY); + FileTemplate template = null; + for (FileTemplate fileTemplate : templates) { + if (fileTemplate.getName().equals("PHP File")) { + template = fileTemplate; + break; + } + } + + if (template != null && viewPsiFile.getViewProvider().getDocument() != null) { + final Properties properties = FileTemplateManager.getDefaultInstance().getDefaultProperties(); + template.setLiveTemplateEnabled(true); + template.setReformatCode(true); + try { + viewPsiFile.getViewProvider().getDocument().insertString(0, template.getText(properties)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/src/com/yii2support/views/inspections/RequireParameterInspection.java b/src/com/yii2support/views/inspections/RequireParameterInspection.java new file mode 100644 index 00000000..ccdb04f7 --- /dev/null +++ b/src/com/yii2support/views/inspections/RequireParameterInspection.java @@ -0,0 +1,98 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.util.ArrayUtil; +import com.jetbrains.php.lang.inspections.PhpInspection; +import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression; +import com.jetbrains.php.lang.psi.elements.FunctionReference; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor; +import com.yii2support.common.PhpUtil; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; + +/** + * Created by NVlad on 23.01.2017. + */ +public class RequireParameterInspection extends PhpInspection { + @NotNull + @Override + public String getShortName() { + return "RequireParameterInspection"; + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder problemsHolder, boolean isOnTheFly) { + return new PhpElementVisitor() { + @Override + public void visitPhpMethodReference(MethodReference reference) { + final String name = reference.getName(); + if (name == null || !ArrayUtil.contains(name, ViewsUtil.renderMethods)) { + return; + } + + final PsiElement[] parameters = reference.getParameters(); + if (parameters.length == 0 || !(parameters[0] instanceof StringLiteralExpression)) { + return; + } + + final PsiFile file = ViewsUtil.getViewFile(parameters[0]); + if (file == null) { + return; + } + + final ArrayList viewParameters = ViewsUtil.getViewVariables(file); + if (viewParameters.size() > 0) { + final Collection existKeys; + if (parameters.length > 1) { + if (parameters[1] instanceof ArrayCreationExpression) { + existKeys = PhpUtil.getArrayKeys((ArrayCreationExpression) parameters[1]); + } else { + existKeys = new HashSet<>(); + if (parameters[1] instanceof FunctionReference) { + FunctionReference function = (FunctionReference) parameters[1]; + if (function.getName() != null && function.getName().equals("compact")) { + for (PsiElement element : function.getParameters()) { + if (element instanceof StringLiteralExpression) { + existKeys.add(((StringLiteralExpression) element).getContents()); + } + } + } + } + } + } else { + existKeys = new HashSet<>(); + } + + if (existKeys.size() == 0 && isOnTheFly) { + String errorRequireParameters = "View %view% require parameters."; + RequireParameterLocalQuickFix fix = new RequireParameterLocalQuickFix(viewParameters); + problemsHolder.registerProblem(reference, errorRequireParameters.replace("%view%", parameters[0].getText()), fix); + return; + } + + viewParameters.removeIf(existKeys::contains); + if (viewParameters.size() > 0) { + String errorRequireParameter = "View %view% require %parameter% parameter"; + for (String parameter : viewParameters) { + RequireParameterLocalQuickFix fix = new RequireParameterLocalQuickFix(viewParameters); + String description = errorRequireParameter + .replace("%view%", parameters[0].getText()) + .replace("%parameter%", parameter); + problemsHolder.registerProblem(reference, description, fix); + } + } + } + } + }; + } +} diff --git a/src/com/yii2support/views/inspections/RequireParameterLocalQuickFix.java b/src/com/yii2support/views/inspections/RequireParameterLocalQuickFix.java new file mode 100644 index 00000000..2ce09466 --- /dev/null +++ b/src/com/yii2support/views/inspections/RequireParameterLocalQuickFix.java @@ -0,0 +1,142 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInsight.template.Template; +import com.intellij.codeInsight.template.TemplateManager; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.jetbrains.php.lang.psi.PhpPsiElementFactory; +import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression; +import com.jetbrains.php.lang.psi.elements.FunctionReference; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Created by NVlad on 15.01.2017. + */ +class RequireParameterLocalQuickFix implements LocalQuickFix { + final private String[] myVariables; + + RequireParameterLocalQuickFix(Collection variables) { + myVariables = variables.toArray(new String[variables.size()]); + } + + @Nls + @NotNull + @Override + public String getName() { + if (myVariables.length == 1) { + return "Add \"%param%\" parameter".replace("%param%", myVariables[0]); + } + + return "Add View parameter(s)"; + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Add View parameter"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + ParameterList parameterList = ((MethodReference) descriptor.getPsiElement()).getParameterList(); + if (parameterList == null) { + return; + } + + PsiElement[] parameters = parameterList.getParameters(); + if (parameters.length == 1) { + ArrayCreationExpression params = PhpPsiElementFactory.createFromText(project, ArrayCreationExpression.class, "[]"); + if (params == null) { + return; + } + parameterList.add(PhpPsiElementFactory.createComma(project)); + parameterList.add(params); + parameters = parameterList.getParameters(); + } + + TemplateManager templateManager = TemplateManager.getInstance(project); + Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); + if (editor == null) { + return; + } + + if (parameters[1] instanceof FunctionReference) { + FunctionReference functionReference = (FunctionReference) parameters[1]; + if (functionReference.getName() == null || !functionReference.getName().equals("compact")) { + return; + } + + Template template = templateManager.createTemplate("", ""); + template.setToReformat(true); + + Boolean firstElement = functionReference.getParameters().length == 0; + for (String variable : myVariables) { + template.addTextSegment(firstElement ? "'" : ", '"); + String var = "$" + variable.toUpperCase() + "$"; + template.addVariable(var, "", "\"" + variable + "\"", true); + template.addVariableSegment(var); + template.addTextSegment("'"); + firstElement = false; + } + + PsiElement psiElement = functionReference.getParameterList(); + if (psiElement != null) { + editor.getCaretModel().moveToOffset(psiElement.getTextRange().getEndOffset()); + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument()); + templateManager.startTemplate(editor, template); + } + + return; + } + + if (!(parameters[1] instanceof ArrayCreationExpression)) { + return; + } + ArrayCreationExpression params = (ArrayCreationExpression) parameters[1]; + + Template template = templateManager.createTemplate("", ""); + template.setToReformat(true); + Boolean addComma = params.getHashElements().iterator().hasNext(); + Boolean newLined = false; + + PsiElement psiElement = params.getLastChild(); + while (psiElement instanceof PsiWhiteSpace || psiElement.getText().equals("]")) { + if (psiElement instanceof PsiWhiteSpace && psiElement.getText().contains("\n")) { + newLined = true; + } + psiElement = psiElement.getPrevSibling(); + } + if (psiElement.getText().equals(",")) { + addComma = false; + } + + for (String variable : myVariables) { + if (addComma) { + template.addTextSegment(","); + } + template.addTextSegment((newLined ? "\n" : " ")); + String templateVariable = "$" + variable.toUpperCase() + "$"; + template.addVariable(templateVariable, "", "\"$" + variable + "\"", true); + template.addTextSegment("'" + variable + "' => "); + template.addVariableSegment(templateVariable); + addComma = true; + } + template.addTextSegment(newLined ? "," : " "); + + editor.getCaretModel().moveToOffset(psiElement.getTextRange().getEndOffset()); + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.getDocument()); + templateManager.startTemplate(editor, template); + } +} diff --git a/src/com/yii2support/views/inspections/UnusedParameterInspection.java b/src/com/yii2support/views/inspections/UnusedParameterInspection.java new file mode 100644 index 00000000..1afedd86 --- /dev/null +++ b/src/com/yii2support/views/inspections/UnusedParameterInspection.java @@ -0,0 +1,120 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.util.ArrayUtil; +import com.jetbrains.php.lang.inspections.PhpInspection; +import com.jetbrains.php.lang.psi.PhpFile; +import com.jetbrains.php.lang.psi.elements.*; +import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Created by NVlad on 15.01.2017. + */ +final public class UnusedParameterInspection extends PhpInspection { + @NotNull + @Override + public String getShortName() { + return "UnusedParameterInspection"; + } + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder problemsHolder, boolean isOnTheFly) { + return new PhpElementVisitor() { + @Override + public void visitPhpMethodReference(MethodReference reference) { + final String name = reference.getName(); + if (name == null || !ArrayUtil.contains(name, ViewsUtil.renderMethods)) { + return; + } + + final PsiElement[] parameters = reference.getParameters(); + if (parameters.length < 2 || !(parameters[0] instanceof StringLiteralExpression)) { + return; + } + + final PsiFile file = ViewsUtil.getViewFile(parameters[0]); + if (file == null) { + return; + } + + if (!(file instanceof PhpFile)) { + return; + } + + final ArrayList viewParameters = ViewsUtil.getViewVariables(file); + if (viewParameters.size() > 0) { + final HashSet unusedParameters = new HashSet<>(); + final String errorUnusedParameter = "View %view% not use \"%parameter%\" parameter"; + if (parameters[1] instanceof ArrayCreationExpression) { + for (ArrayHashElement element : ((ArrayCreationExpression) parameters[1]).getHashElements()) { + if (element.getKey() != null && element.getKey() instanceof StringLiteralExpression) { + final String key = ((StringLiteralExpression) element.getKey()).getContents(); + if (!viewParameters.contains(key)) { + UnusedParameterLocalQuickFix fix = new UnusedParameterLocalQuickFix(key); + String description = errorUnusedParameter + .replace("%view%", parameters[0].getText()) + .replace("%parameter%", key); + problemsHolder.registerProblem(element, description, ProblemHighlightType.LIKE_UNUSED_SYMBOL, fix); + unusedParameters.add(key); + } + } + } + } + if (parameters[1] instanceof FunctionReference) { + FunctionReference function = ((FunctionReference) parameters[1]); + if (function.getName() != null && function.getName().contains("compact")) { + for (PsiElement element : function.getParameters()) { + if (element instanceof StringLiteralExpression) { + String key = ((StringLiteralExpression) element).getContents(); + if (!viewParameters.contains(key)) { + UnusedParameterLocalQuickFix fix = new UnusedParameterLocalQuickFix(key); + String description = errorUnusedParameter + .replace("%view%", parameters[0].getText()) + .replace("%parameter%", key); + problemsHolder.registerProblem(element, description, ProblemHighlightType.LIKE_UNUSED_SYMBOL, fix); + unusedParameters.add(key); + } + } + } + } + } + + if (unusedParameters.size() > 0 && isOnTheFly) { + if (viewParameters.containsAll(unusedParameters)) { + String errorUnusedParameters = "This View does not use parameters"; + UnusedParametersLocalQuickFix fix = new UnusedParametersLocalQuickFix(); + problemsHolder.registerProblem(parameters[1], errorUnusedParameters, ProblemHighlightType.LIKE_UNUSED_SYMBOL, fix); + problemsHolder.registerProblem(reference, errorUnusedParameters, ProblemHighlightType.INFORMATION, fix); + } else { + String errorUnusedParameters = "This View have unused parameters"; + UnusedParametersLocalQuickFix fix = new UnusedParametersLocalQuickFix(unusedParameters); + problemsHolder.registerProblem(reference, errorUnusedParameters, ProblemHighlightType.INFORMATION, fix); + } + + } + } else { + if (parameters.length > 1) { + if (parameters[1] instanceof ArrayCreationExpression || parameters[1] instanceof FunctionReference) { + String errorUnusedParameters = "This View does not use parameters"; + UnusedParametersLocalQuickFix fix = new UnusedParametersLocalQuickFix(); + problemsHolder.registerProblem(parameters[1], errorUnusedParameters, ProblemHighlightType.LIKE_UNUSED_SYMBOL, fix); + if (isOnTheFly) { + problemsHolder.registerProblem(reference, errorUnusedParameters, ProblemHighlightType.INFORMATION, fix); + } + } + } + } + } + }; + } +} diff --git a/src/com/yii2support/views/inspections/UnusedParameterLocalQuickFix.java b/src/com/yii2support/views/inspections/UnusedParameterLocalQuickFix.java new file mode 100644 index 00000000..a9ba518c --- /dev/null +++ b/src/com/yii2support/views/inspections/UnusedParameterLocalQuickFix.java @@ -0,0 +1,68 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression; +import com.jetbrains.php.lang.psi.elements.FunctionReference; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.yii2support.common.PsiUtil; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +/** + * Created by NVlad on 16.01.2017. + */ +class UnusedParameterLocalQuickFix implements LocalQuickFix { + final private String myParam; + + UnusedParameterLocalQuickFix(String param) { + myParam = param; + } + + @Nls + @NotNull + @Override + public String getName() { + return "Remove unused parameter \"%param%\"".replace("%param%", myParam); + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Remove unused parameter"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement item = descriptor.getPsiElement(); + + PsiElement context = item.getContext(); + if (context instanceof ArrayCreationExpression) { + ArrayCreationExpression params = (ArrayCreationExpression) item.getParent(); + + PsiUtil.deleteArrayElement(item); + + if (!params.getHashElements().iterator().hasNext()) { + if (params.getPrevSibling() instanceof PsiWhiteSpace) { + params.getPrevSibling().delete(); + } + params.getPrevSibling().delete(); + params.delete(); + } + } + if (context instanceof ParameterList && context.getParent() instanceof FunctionReference) { + FunctionReference functionReference = (FunctionReference) context.getParent(); + if (functionReference.getName() != null && functionReference.getName().equals("compact")) { + PsiUtil.deleteFunctionParam(item); + + if (functionReference.getParameters().length == 0) { + PsiUtil.deleteFunctionParam(functionReference); + } + } + } + } +} diff --git a/src/com/yii2support/views/inspections/UnusedParametersLocalQuickFix.java b/src/com/yii2support/views/inspections/UnusedParametersLocalQuickFix.java new file mode 100644 index 00000000..7c838b5e --- /dev/null +++ b/src/com/yii2support/views/inspections/UnusedParametersLocalQuickFix.java @@ -0,0 +1,78 @@ +package com.yii2support.views.inspections; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.jetbrains.php.lang.psi.elements.*; +import com.yii2support.common.PsiUtil; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashSet; + +/** + * Created by NVlad on 16.01.2017. + */ +class UnusedParametersLocalQuickFix implements LocalQuickFix { + private HashSet myUnusedParams; + + UnusedParametersLocalQuickFix() { + } + + UnusedParametersLocalQuickFix(HashSet unusedParams) { + myUnusedParams = unusedParams; + } + + @Nls + @NotNull + @Override + public String getFamilyName() { + return "Remove all unused parameters"; + } + + @Override + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { + PsiElement viewParameters = descriptor.getPsiElement(); + if (viewParameters instanceof MethodReference) { + viewParameters = ((MethodReference) viewParameters).getParameters()[1]; + } + + if (myUnusedParams != null) { + final ArrayList unused = new ArrayList<>(); + if (viewParameters instanceof ArrayCreationExpression) { + for (ArrayHashElement element : ((ArrayCreationExpression) viewParameters).getHashElements()) { + if (element.getKey() != null) { + final String key = ((StringLiteralExpression) element.getKey()).getContents(); + if (myUnusedParams.contains(key)) { + unused.add(element); + } + } + } + for (PsiElement element : unused) { + PsiUtil.deleteArrayElement(element); + } + } + if (viewParameters instanceof FunctionReference) { + for (PsiElement element : ((FunctionReference) viewParameters).getParameters()) { + if (element instanceof StringLiteralExpression) { + if (myUnusedParams.contains(((StringLiteralExpression) element).getContents())) { + unused.add(element); + } + } + } + for (PsiElement element : unused) { + PsiUtil.deleteFunctionParam(element); + } + } + } else { + if (viewParameters.getPrevSibling() instanceof PsiWhiteSpace) { + viewParameters.getPrevSibling().delete(); + } + viewParameters.getPrevSibling().delete(); + viewParameters.delete(); + } + } +} diff --git a/src/com/yii2support/views/refactor/RenameViewProcessor.java b/src/com/yii2support/views/refactor/RenameViewProcessor.java new file mode 100644 index 00000000..f99a09bc --- /dev/null +++ b/src/com/yii2support/views/refactor/RenameViewProcessor.java @@ -0,0 +1,86 @@ +package com.yii2support.views.refactor; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import com.intellij.refactoring.listeners.RefactoringElementListener; +import com.intellij.refactoring.rename.RenamePsiElementProcessor; +import com.jetbrains.php.lang.psi.PhpPsiElementFactory; +import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; + +/** + * Created by NVlad on 02.02.2017. + */ +public class RenameViewProcessor extends RenamePsiElementProcessor { + private final static Key WITH_EXT = Key.create("RenameViewProcessor.withExt"); + private final static Key OLD_EXT = Key.create("RenameViewProcessor.oldExt"); + private final static Key RELATIVE_PATH = Key.create("RenameViewProcessor.relativePath"); + + private final HashSet renders = new HashSet<>(); + + @Override + public boolean canProcessElement(@NotNull PsiElement psiElement) { + return true; + } + + @Override + public void prepareRenaming(PsiElement psiElement, String s, Map map) { + renders.clear(); + for (PsiReference reference : findReferences(psiElement)) { + final PsiElement element = reference.getElement(); + if (element instanceof StringLiteralExpression) { + String fileName = ((StringLiteralExpression) element).getContents(); + if (fileName.contains("/")) { + element.getParent().putUserData(RELATIVE_PATH, fileName.substring(0, fileName.lastIndexOf('/') + 1)); + fileName = fileName.substring(fileName.lastIndexOf('/') + 1); + } + element.getParent().putUserData(WITH_EXT, fileName.contains(".")); + + String realFile = ((PsiFile) psiElement).getName(); + if (realFile.contains(".")) { + element.getParent().putUserData(OLD_EXT, realFile.substring(realFile.lastIndexOf('.'))); + } + + renders.add(element.getParent()); + } + } + } + + @Nullable + @Override + public Runnable getPostRenameCallback(PsiElement psiElement, String s, RefactoringElementListener refactoringElementListener) { + return () -> { + for (PsiElement render : renders) { + StringLiteralExpression element = (StringLiteralExpression) ((ParameterList) render).getParameters()[0]; + final PsiElement parent = element.getParent(); + String fileName = element.getContents(); + if (Objects.equals(parent.getUserData(WITH_EXT), false)) { + if (Objects.equals(fileName.substring(fileName.lastIndexOf('.')), parent.getUserData(OLD_EXT))) { + fileName = fileName.substring(0, fileName.lastIndexOf('.')); + } + } + parent.putUserData(WITH_EXT, null); + parent.putUserData(OLD_EXT, null); + + if (parent.getUserData(RELATIVE_PATH) != null) { + fileName = parent.getUserData(RELATIVE_PATH) + fileName; + parent.putUserData(RELATIVE_PATH, null); + } + + fileName = element.isSingleQuote() ? "'" + fileName + "'" : "\"" + fileName + "\""; + PsiElement newValue = PhpPsiElementFactory.createFromText(psiElement.getProject(), StringLiteralExpression.class, fileName); + if (newValue != null) { + element.replace(newValue); + } + } + }; + } +} diff --git a/src/com/yii2support/views/references/PsiReference.java b/src/com/yii2support/views/references/PsiReference.java new file mode 100644 index 00000000..6338dc17 --- /dev/null +++ b/src/com/yii2support/views/references/PsiReference.java @@ -0,0 +1,83 @@ +package com.yii2support.views.references; + +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ArrayUtil; +import com.intellij.util.IncorrectOperationException; +import com.jetbrains.php.lang.psi.PhpPsiElementFactory; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import com.yii2support.views.ViewsUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Created by NVlad on 02.01.2017. + */ +public class PsiReference extends PsiReferenceBase { + PsiReference(@NotNull PsiElement element) { + super(element); + } + + @Nullable + @Override + public PsiElement resolve() { + return ViewsUtil.getViewFile(myElement); + } + + @NotNull + @Override + public Object[] getVariants() { + return new Object[0]; + } + + @Override + public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { + final StringLiteralExpression string = (StringLiteralExpression) this.getElement(); + final PsiDirectory context = ViewsUtil.getContextDirectory(string); + final PsiFile file = (PsiFile) element; + final PsiElement newValue; + + String fileName = string.getContents(); + if (fileName.contains("/")) { + fileName = fileName.substring(fileName.lastIndexOf('/') + 1); + } + if (!file.getContainingDirectory().equals(context)) { + final PsiDirectory root = ViewsUtil.getRootDirectory(string); + if (root == null) { + return null; + } + + PsiDirectory dir = file.getContainingDirectory(); + while (dir != null && !(dir.equals(root) || dir.equals(context))) { + fileName = dir.getName() + "/" + fileName; + dir = dir.getParent(); + } + + if (dir == null) { + return null; + } + + if (dir.equals(root)) { + fileName = "/" + fileName; + } + } + fileName = string.isSingleQuote() ? "'" + fileName + "'" : "\"" + fileName + "\""; + newValue = PhpPsiElementFactory.createFromText(element.getProject(), StringLiteralExpression.class, fileName); + + if (newValue != null) { + string.replace(newValue); + } + + for (MethodReference reference : PsiTreeUtil.findChildrenOfType(file, MethodReference.class)) { + if (reference.getName() != null && ArrayUtil.contains(reference.getName(), ViewsUtil.renderMethods)) { + reference.putUserData(ViewsUtil.RENDER_VIEW_FILE, null); + } + } + + return newValue; + } +} diff --git a/src/com/yii2support/views/PsiReferenceContributor.java b/src/com/yii2support/views/references/PsiReferenceContributor.java similarity index 69% rename from src/com/yii2support/views/PsiReferenceContributor.java rename to src/com/yii2support/views/references/PsiReferenceContributor.java index 00fe9be5..ad951e56 100644 --- a/src/com/yii2support/views/PsiReferenceContributor.java +++ b/src/com/yii2support/views/references/PsiReferenceContributor.java @@ -1,11 +1,11 @@ -package com.yii2support.views; +package com.yii2support.views.references; import com.intellij.patterns.ElementPattern; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiReferenceRegistrar; -import com.jetbrains.php.lang.psi.elements.MethodReference; -import com.jetbrains.php.lang.psi.elements.ParameterList; +import com.yii2support.common.Patterns; +import com.yii2support.views.ViewsUtil; import org.jetbrains.annotations.NotNull; /** @@ -19,7 +19,6 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar psiReferen private static ElementPattern ElementPattern() { return PlatformPatterns.psiElement(PsiElement.class) - .withParent(PlatformPatterns.psiElement(ParameterList.class) - .withParent(PlatformPatterns.psiElement(MethodReference.class))); + .withSuperParent(2, Patterns.methodWithName(ViewsUtil.renderMethods)); } } diff --git a/src/com/yii2support/views/PsiReferenceProvider.java b/src/com/yii2support/views/references/PsiReferenceProvider.java similarity index 83% rename from src/com/yii2support/views/PsiReferenceProvider.java rename to src/com/yii2support/views/references/PsiReferenceProvider.java index 7f45aef3..a295a229 100644 --- a/src/com/yii2support/views/PsiReferenceProvider.java +++ b/src/com/yii2support/views/references/PsiReferenceProvider.java @@ -1,4 +1,4 @@ -package com.yii2support.views; +package com.yii2support.views.references; import com.intellij.psi.PsiElement; import com.intellij.util.ProcessingContext; @@ -10,7 +10,7 @@ /** * Created by NVlad on 02.01.2017. */ -public class PsiReferenceProvider extends com.intellij.psi.PsiReferenceProvider { +class PsiReferenceProvider extends com.intellij.psi.PsiReferenceProvider { @NotNull @Override public PsiReference[] getReferencesByElement(@NotNull PsiElement psiElement, @NotNull ProcessingContext processingContext) {