diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e038f762..258ba6980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * MFTF support (reference navigation, completion) * Fixed support of 2020.* versions of IDE's * Create a New Magento 2 Module action + * Code Inspection: Duplicated Observer Usage in events XML 0.3.0 ============= diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 5192bfbc6..10b1a6b8f 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -102,8 +102,19 @@ + + + diff --git a/resources/inspectionDescriptions/ObserverDeclarationInspection.html b/resources/inspectionDescriptions/ObserverDeclarationInspection.html new file mode 100644 index 000000000..f14b67d76 --- /dev/null +++ b/resources/inspectionDescriptions/ObserverDeclarationInspection.html @@ -0,0 +1,23 @@ + + + + + +

Observer names for events must be unique, otherwise overriding occurs. Overrides can be done by accident.

+

+ In case overriding is wanted, + + disable the original observer + + and give a unique name to the current observer. +

+

+ Read more about Events & Observers +

+ + \ No newline at end of file diff --git a/src/com/magento/idea/magento2plugin/inspections/xml/ObserverDeclarationInspection.java b/src/com/magento/idea/magento2plugin/inspections/xml/ObserverDeclarationInspection.java new file mode 100644 index 000000000..a29b8727a --- /dev/null +++ b/src/com/magento/idea/magento2plugin/inspections/xml/ObserverDeclarationInspection.java @@ -0,0 +1,280 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +package com.magento.idea.magento2plugin.inspections.xml; + +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.ide.highlighter.XmlFileType; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.*; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlAttributeValue; +import com.intellij.psi.xml.XmlDocument; +import com.intellij.psi.xml.XmlTag; +import com.jetbrains.php.lang.inspections.PhpInspection; +import com.magento.idea.magento2plugin.indexes.EventIndex; +import com.magento.idea.magento2plugin.magento.files.ModuleXml; +import com.magento.idea.magento2plugin.magento.packages.MagentoPackages; +import org.jetbrains.annotations.NotNull; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.*; + +import com.intellij.openapi.vfs.VfsUtil; +import org.jetbrains.annotations.Nullable; + +public class ObserverDeclarationInspection extends PhpInspection { + + @NotNull + @Override + public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder problemsHolder, boolean b) { + return new XmlElementVisitor() { + private final String moduleXmlFileName = ModuleXml.getInstance().getFileName(); + private static final String eventsXmlFileName = "events.xml"; + private static final String duplicatedObserverNameSameFileProblemDescription = "The observer name already used in this file. For more details see Inspection Description."; + private static final String duplicatedObserverNameProblemDescription = + "The observer name \"%s\" for event \"%s\" is already used in the module \"%s\" (%s scope). For more details see Inspection Description."; + private HashMap loadedFileHash = new HashMap<>(); + private final ProblemHighlightType errorSeverity = ProblemHighlightType.WARNING; + + @Override + public void visitFile(PsiFile file) { + if (!file.getName().equals(eventsXmlFileName)) { + return; + } + + XmlTag[] xmlTags = getFileXmlTags(file); + EventIndex eventIndex = EventIndex.getInstance(file.getProject()); + + if (xmlTags == null) { + return; + } + + HashMap targetObserversHash = new HashMap<>(); + + for (XmlTag eventXmlTag: xmlTags) { + HashMap eventProblems = new HashMap<>(); + if (!eventXmlTag.getName().equals("event")) { + continue; + } + + XmlAttribute eventNameAttribute = eventXmlTag.getAttribute("name"); + + String eventNameAttributeValue = eventNameAttribute.getValue(); + if (eventNameAttributeValue == null) { + continue; + } + + List targetObservers = fetchObserverTagsFromEventTag(eventXmlTag); + + for (XmlTag observerXmlTag: targetObservers) { + XmlAttribute observerNameAttribute = observerXmlTag.getAttribute("name"); + XmlAttribute observerDisabledAttribute = observerXmlTag.getAttribute("disabled"); + + if (observerNameAttribute == null || (observerDisabledAttribute != null && observerDisabledAttribute.getValue().equals("true"))) { + continue; + } + + String observerName = observerNameAttribute.getValue(); + String observerKey = eventNameAttributeValue.concat("_").concat(observerName); + if (targetObserversHash.containsKey(observerKey)) { + problemsHolder.registerProblem( + observerNameAttribute.getValueElement(), + duplicatedObserverNameSameFileProblemDescription, + errorSeverity + ); + } + targetObserversHash.put(observerKey, observerXmlTag); + + List> modulesWithSameObserverName = fetchModuleNamesWhereSameObserverNameUsed(eventNameAttributeValue, observerName, eventIndex, file); + for (HashMap moduleEntry: modulesWithSameObserverName) { + Map.Entry module = moduleEntry.entrySet().iterator().next(); + String moduleName = module.getKey(); + String scope = module.getValue(); + String problemKey = observerKey.concat("_").concat(moduleName).concat("_").concat(scope); + if (!eventProblems.containsKey(problemKey)){ + problemsHolder.registerProblem( + observerNameAttribute.getValueElement(), + String.format( + duplicatedObserverNameProblemDescription, + observerName, + eventNameAttributeValue, + moduleName, + scope + ), + errorSeverity + ); + eventProblems.put(problemKey, observerXmlTag); + } + } + } + } + } + + private List> fetchModuleNamesWhereSameObserverNameUsed(String eventNameAttributeValue, String observerName, EventIndex eventIndex, PsiFile file) { + List> modulesName = new ArrayList<>(); + String currentFileDirectory = file.getContainingDirectory().toString(); + String currentFileFullPath = currentFileDirectory.concat("/").concat(file.getName()); + + Collection indexedEvents = eventIndex.getEventElements(eventNameAttributeValue, GlobalSearchScope.getScopeRestrictedByFileTypes( + GlobalSearchScope.allScope(file.getProject()), + XmlFileType.INSTANCE + )); + + for (PsiElement indexedEvent: indexedEvents) { + PsiFile indexedAttributeParent = PsiTreeUtil.getTopmostParentOfType(indexedEvent, PsiFile.class); + if (indexedAttributeParent == null) { + continue; + } + + String indexedEventAttributeValue = ((XmlAttributeValue) indexedEvent).getValue(); + if (!indexedEventAttributeValue.equals(eventNameAttributeValue)) { + continue; + } + + String indexedFileDirectory = indexedAttributeParent.getContainingDirectory().toString(); + String indexedFileFullPath = indexedFileDirectory.concat("/").concat(indexedAttributeParent.getName()); + if (indexedFileFullPath.equals(currentFileFullPath)) { + continue; + } + + String scope = getAreaFromFileDirectory(indexedAttributeParent); + + List indexObserversTags = fetchObserverTagsFromEventTag((XmlTag) indexedEvent.getParent().getParent()); + for (XmlTag indexObserversTag: indexObserversTags) { + XmlAttribute indexedObserverNameAttribute = indexObserversTag.getAttribute("name"); + if (indexedObserverNameAttribute == null) { + continue; + } + if (!observerName.equals(indexedObserverNameAttribute.getValue())){ + continue; + } + addModuleNameWhereSameObserverUsed(modulesName, indexedAttributeParent, scope); + } + } + + return modulesName; + } + + private List fetchObserverTagsFromEventTag(XmlTag eventXmlTag) { + List result = new ArrayList<>(); + XmlTag[] observerXmlTags = PsiTreeUtil.getChildrenOfType(eventXmlTag, XmlTag.class); + if (observerXmlTags == null) { + return result; + } + + for (XmlTag observerXmlTag: observerXmlTags) { + if (!observerXmlTag.getName().equals("observer")) { + continue; + } + + result.add(observerXmlTag); + } + + return result; + } + + private void addModuleNameWhereSameObserverUsed(List> modulesName, PsiFile indexedFile, String scope) { + XmlTag moduleDeclarationTag = getModuleDeclarationTagByConfigFile(indexedFile); + if (moduleDeclarationTag == null) return; + + if (!moduleDeclarationTag.getName().equals("module")) { + return; + } + XmlAttribute moduleNameAttribute = moduleDeclarationTag.getAttribute("name"); + if (moduleNameAttribute == null) { + return; + } + + HashMap moduleEntry = new HashMap<>(); + + moduleEntry.put(moduleNameAttribute.getValue(), scope); + modulesName.add(moduleEntry); + } + + @Nullable + private XmlTag getModuleDeclarationTagByConfigFile(PsiFile file) { + String fileDirectory = file.getContainingDirectory().toString(); + String fileArea = file.getContainingDirectory().getName(); + String moduleXmlFilePath = getModuleXmlFilePathByConfigFileDirectory(fileDirectory, fileArea); + + VirtualFile virtualFile = getFileByPath(moduleXmlFilePath); + if (virtualFile == null) return null; + + PsiFile moduleDeclarationFile = PsiManager.getInstance(file.getProject()).findFile(virtualFile); + XmlTag[] moduleDeclarationTags = getFileXmlTags(moduleDeclarationFile); + if (moduleDeclarationTags == null) { + return null; + } + return moduleDeclarationTags[0]; + } + + @Nullable + private VirtualFile getFileByPath(String moduleXmlFilePath) { + if (loadedFileHash.containsKey(moduleXmlFilePath)) { + return loadedFileHash.get(moduleXmlFilePath); + } + VirtualFile virtualFile; + try { + virtualFile = VfsUtil.findFileByURL(new URL(moduleXmlFilePath)); + } catch (MalformedURLException e) { + return null; + } + if (virtualFile == null) { + return null; + } + loadedFileHash.put(moduleXmlFilePath, virtualFile); + return virtualFile; + } + + private String getModuleXmlFilePathByConfigFileDirectory(String fileDirectory, String fileArea) { + String moduleXmlFile = fileDirectory.replace(fileArea, "").concat(moduleXmlFileName); + if (fileDirectory.endsWith("etc")) { + moduleXmlFile = fileDirectory.concat("/").concat(moduleXmlFileName); + } + return moduleXmlFile.replace("PsiDirectory:", "file:"); + } + + @Nullable + private XmlTag[] getFileXmlTags(PsiFile file) { + XmlDocument xmlDocument = PsiTreeUtil.getChildOfType(file, XmlDocument.class); + XmlTag xmlRootTag = PsiTreeUtil.getChildOfType(xmlDocument, XmlTag.class); + return PsiTreeUtil.getChildrenOfType(xmlRootTag, XmlTag.class); + } + + private String getAreaFromFileDirectory(@NotNull PsiFile file) { + if (file.getParent() == null) { + return ""; + } + + String areaFromFileDirectory = file.getParent().getName(); + + if (areaFromFileDirectory.equals("etc")) { + return MagentoPackages.AREA_BASE; + } + + List possibleAreas = new ArrayList<>(Arrays.asList( + MagentoPackages.AREA_ADMINHTML, + MagentoPackages.AREA_FRONTEND, + MagentoPackages.AREA_CRON, + MagentoPackages.AREA_API_REST, + MagentoPackages.AREA_API_SOAP, + MagentoPackages.AREA_GRAPHQL + )); + + for (String area: possibleAreas) { + if (area.equals(areaFromFileDirectory)) { + return area; + } + } + + return ""; + } + }; + } +} diff --git a/src/com/magento/idea/magento2plugin/magento/packages/MagentoPackages.java b/src/com/magento/idea/magento2plugin/magento/packages/MagentoPackages.java index 4df54a8f3..9a09eeb3b 100644 --- a/src/com/magento/idea/magento2plugin/magento/packages/MagentoPackages.java +++ b/src/com/magento/idea/magento2plugin/magento/packages/MagentoPackages.java @@ -6,4 +6,11 @@ public class MagentoPackages { public static String PACKAGES_ROOT = "app/code"; + public static String AREA_BASE = "base"; + public static String AREA_ADMINHTML = "adminhtml"; + public static String AREA_FRONTEND = "frontend"; + public static String AREA_CRON = "crontab"; + public static String AREA_API_REST = "webapi_rest"; + public static String AREA_API_SOAP = "webapi_soap"; + public static String AREA_GRAPHQL = "graphql"; }