diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index cacaa831f..6f5a33fba 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -116,6 +116,7 @@ + @@ -169,6 +170,13 @@ enabledByDefault="true" level="ERROR" implementationClass="com.magento.idea.magento2plugin.inspections.xml.ModuleDeclarationInModuleXmlInspection"/> + + diff --git a/resources/magento2/inspection.properties b/resources/magento2/inspection.properties index 58cbb0e40..15d190ce3 100644 --- a/resources/magento2/inspection.properties +++ b/resources/magento2/inspection.properties @@ -16,3 +16,5 @@ inspection.observer.duplicateInOtherPlaces=The observer name "{0}" for event "{1 inspection.cache.disabledCache=Cacheable false attribute on the default layout will disable cache site-wide inspection.moduleDeclaration.warning.wrongModuleName=Provided module name "{0}" does not match expected "{1}" inspection.moduleDeclaration.fix=Fix module name +inspection.aclResource.error.missingAttribute=Attribute "{0}" is required +inspection.aclResource.error.idAttributeCanNotBeEmpty=Attribute value "{0}" can not be empty diff --git a/src/com/magento/idea/magento2plugin/indexes/IndexManager.java b/src/com/magento/idea/magento2plugin/indexes/IndexManager.java index 27e4612e4..b6caa5190 100644 --- a/src/com/magento/idea/magento2plugin/indexes/IndexManager.java +++ b/src/com/magento/idea/magento2plugin/indexes/IndexManager.java @@ -2,21 +2,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + package com.magento.idea.magento2plugin.indexes; import com.intellij.util.indexing.FileBasedIndexImpl; import com.intellij.util.indexing.ID; -import com.magento.idea.magento2plugin.stubs.indexes.*; +import com.magento.idea.magento2plugin.stubs.indexes.BlockNameIndex; +import com.magento.idea.magento2plugin.stubs.indexes.ContainerNameIndex; +import com.magento.idea.magento2plugin.stubs.indexes.EventNameIndex; +import com.magento.idea.magento2plugin.stubs.indexes.EventObserverIndex; +import com.magento.idea.magento2plugin.stubs.indexes.ModuleNameIndex; +import com.magento.idea.magento2plugin.stubs.indexes.ModulePackageIndex; import com.magento.idea.magento2plugin.stubs.indexes.PluginIndex; +import com.magento.idea.magento2plugin.stubs.indexes.VirtualTypeIndex; +import com.magento.idea.magento2plugin.stubs.indexes.WebApiTypeIndex; +import com.magento.idea.magento2plugin.stubs.indexes.graphql.GraphQlResolverIndex; import com.magento.idea.magento2plugin.stubs.indexes.js.MagentoLibJsIndex; import com.magento.idea.magento2plugin.stubs.indexes.js.RequireJsIndex; -import com.magento.idea.magento2plugin.stubs.indexes.graphql.GraphQlResolverIndex; -import com.magento.idea.magento2plugin.stubs.indexes.mftf.*; +import com.magento.idea.magento2plugin.stubs.indexes.mftf.ActionGroupIndex; +import com.magento.idea.magento2plugin.stubs.indexes.mftf.DataIndex; +import com.magento.idea.magento2plugin.stubs.indexes.mftf.PageIndex; +import com.magento.idea.magento2plugin.stubs.indexes.mftf.SectionIndex; +import com.magento.idea.magento2plugin.stubs.indexes.mftf.TestNameIndex; +import com.magento.idea.magento2plugin.stubs.indexes.xml.AclResourceIndex; import com.magento.idea.magento2plugin.stubs.indexes.xml.PhpClassNameIndex; +@SuppressWarnings({"PMD.ClassNamingConventions", "PMD.UseUtilityClass"}) public class IndexManager { + + /** + * Refresh Magento 2 indexes. + */ public static void manualReindex() { - ID[] indexIds = new ID[] { + final ID[] indexIds = new ID[] {//NOPMD // php ModulePackageIndex.KEY, // xml|di configuration @@ -32,6 +50,8 @@ public static void manualReindex() { WebApiTypeIndex.KEY, ModuleNameIndex.KEY, PhpClassNameIndex.KEY, + //acl + AclResourceIndex.KEY, //require_js RequireJsIndex.KEY, MagentoLibJsIndex.KEY, @@ -41,15 +61,15 @@ public static void manualReindex() { PageIndex.KEY, SectionIndex.KEY, TestNameIndex.KEY, - //graphql + //graphql GraphQlResolverIndex.KEY }; - for (ID id: indexIds) { + for (final ID id: indexIds) { try { FileBasedIndexImpl.getInstance().requestRebuild(id); - FileBasedIndexImpl.getInstance().scheduleRebuild(id, new Throwable()); - } catch (NullPointerException exception) { + FileBasedIndexImpl.getInstance().scheduleRebuild(id, new Throwable());//NOPMD + } catch (NullPointerException exception) { //NOPMD //that's fine, indexer is not present in map java.util.Map.get } } diff --git a/src/com/magento/idea/magento2plugin/inspections/xml/AclResourceXmlInspection.java b/src/com/magento/idea/magento2plugin/inspections/xml/AclResourceXmlInspection.java new file mode 100644 index 000000000..78bedca70 --- /dev/null +++ b/src/com/magento/idea/magento2plugin/inspections/xml/AclResourceXmlInspection.java @@ -0,0 +1,92 @@ +/* + * 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.codeInspection.XmlSuppressableInspectionTool; +import com.intellij.psi.PsiElementVisitor; +import com.intellij.psi.PsiFile; +import com.intellij.psi.XmlElementVisitor; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.xml.XmlAttribute; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.indexing.FileBasedIndex; +import com.magento.idea.magento2plugin.bundles.InspectionBundle; +import com.magento.idea.magento2plugin.magento.files.ModuleAclXml; +import com.magento.idea.magento2plugin.stubs.indexes.xml.AclResourceIndex; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +public class AclResourceXmlInspection extends XmlSuppressableInspectionTool { + + @NotNull + @Override + public PsiElementVisitor buildVisitor( + final @NotNull ProblemsHolder problemsHolder, + final boolean isOnTheFly + ) { + return new XmlElementVisitor() { + private final InspectionBundle inspectionBundle = new InspectionBundle(); + + @Override + public void visitXmlTag(final XmlTag xmlTag) { + final PsiFile file = xmlTag.getContainingFile(); + final String filename = file.getName(); + if (!filename.equals(ModuleAclXml.FILE_NAME)) { + return; + } + + if (!xmlTag.getName().equals(ModuleAclXml.XML_TAG_RESOURCE)) { + return; + } + + final XmlAttribute identifier = xmlTag.getAttribute(ModuleAclXml.XML_ATTR_ID); + if (identifier == null) { + //should be handled by schema + return; + } + + final String idValue = identifier.getValue(); + if (idValue == null || idValue.isEmpty()) { + problemsHolder.registerProblem( + identifier, + inspectionBundle.message( + "inspection.aclResource.error.idAttributeCanNotBeEmpty", + "id" + ), + ProblemHighlightType.WARNING + ); + return; + } + + final XmlAttribute title = xmlTag.getAttribute(ModuleAclXml.XML_ATTR_TITLE); + if (title != null && title.getValue() != null) { + return; + } + + final List titles = + FileBasedIndex.getInstance().getValues( + AclResourceIndex.KEY, + idValue, + GlobalSearchScope.allScope(file.getProject() + ) + ); + + if (titles.isEmpty()) { + problemsHolder.registerProblem( + identifier, + inspectionBundle.message( + "inspection.aclResource.error.missingAttribute", + "title" + ), + ProblemHighlightType.WARNING + ); + } + } + }; + } +} diff --git a/src/com/magento/idea/magento2plugin/magento/files/ModuleAclXml.java b/src/com/magento/idea/magento2plugin/magento/files/ModuleAclXml.java index d88b1e63a..71d3fe0c6 100644 --- a/src/com/magento/idea/magento2plugin/magento/files/ModuleAclXml.java +++ b/src/com/magento/idea/magento2plugin/magento/files/ModuleAclXml.java @@ -2,9 +2,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + package com.magento.idea.magento2plugin.magento.files; +@SuppressWarnings({"PMD.ClassNamingConventions", "PMD.FieldNamingConventions"}) public class ModuleAclXml { public static String XML_ATTR_ID = "id"; + public static String XML_ATTR_TITLE = "title"; + public static String XML_TAG_RESOURCE = "resource"; + public static String XML_TAG_RESOURCES = "resources"; + public static String XML_TAG_ACL = "acl"; public static String FILE_NAME = "acl.xml"; } diff --git a/src/com/magento/idea/magento2plugin/stubs/indexes/xml/AclResourceIndex.java b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/AclResourceIndex.java new file mode 100644 index 000000000..b780114d8 --- /dev/null +++ b/src/com/magento/idea/magento2plugin/stubs/indexes/xml/AclResourceIndex.java @@ -0,0 +1,113 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +package com.magento.idea.magento2plugin.stubs.indexes.xml; + +import com.intellij.ide.highlighter.XmlFileType; +import com.intellij.psi.PsiFile; +import com.intellij.psi.xml.XmlDocument; +import com.intellij.psi.xml.XmlFile; +import com.intellij.psi.xml.XmlTag; +import com.intellij.util.indexing.DataIndexer; +import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.FileBasedIndexExtension; +import com.intellij.util.indexing.FileContent; +import com.intellij.util.indexing.ID; +import com.intellij.util.io.DataExternalizer; +import com.intellij.util.io.EnumeratorStringDescriptor; +import com.intellij.util.io.KeyDescriptor; +import com.intellij.util.xml.impl.DomApplicationComponent; +import com.magento.idea.magento2plugin.magento.files.ModuleAclXml; +import com.magento.idea.magento2plugin.project.Settings; +import gnu.trove.THashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; + +public class AclResourceIndex extends FileBasedIndexExtension { + public static final ID KEY = ID.create( + "com.magento.idea.magento2plugin.stubs.indexes.acl_resources"); + private final KeyDescriptor myKeyDescriptor = new EnumeratorStringDescriptor(); + + @NotNull + @Override + public DataIndexer getIndexer() { + return inputData -> { + final Map map = new THashMap<>();//NOPMD + final PsiFile psiFile = inputData.getPsiFile(); + if (!Settings.isEnabled(psiFile.getProject())) { + return map; + } + + if (psiFile instanceof XmlFile) { + final XmlDocument xmlDocument = ((XmlFile) psiFile).getDocument(); + if (xmlDocument != null) { + final XmlTag xmlRootTag = xmlDocument.getRootTag(); + if (xmlRootTag != null) { //NOPMD + parseRootTag(map, xmlRootTag); + } + } + } + return map; + }; + } + + protected void parseRootTag(final Map map, final XmlTag xmlRootTag) { + for (final XmlTag aclTag : xmlRootTag.findSubTags(ModuleAclXml.XML_TAG_ACL)) { + for (final XmlTag resourcesTag : aclTag.findSubTags(ModuleAclXml.XML_TAG_RESOURCES)) { + parseResourceTag(map, resourcesTag); + } + } + } + + private void parseResourceTag(final Map map, final XmlTag resourcesTag) { + for (final XmlTag resourceTag : resourcesTag.findSubTags(ModuleAclXml.XML_TAG_RESOURCE)) { + final String identifier = resourceTag.getAttributeValue(ModuleAclXml.XML_ATTR_ID); + final String title = resourceTag.getAttributeValue(ModuleAclXml.XML_ATTR_TITLE); + + if (identifier != null && title != null && !identifier.isEmpty() + && !title.isEmpty()) { + map.put(identifier, title); + } + + parseResourceTag(map, resourceTag); + } + } + + @NotNull + @Override + public ID getName() { + return KEY; + } + + @NotNull + @Override + public KeyDescriptor getKeyDescriptor() { + return this.myKeyDescriptor; + } + + @NotNull + @Override + public DataExternalizer getValueExternalizer() { + return EnumeratorStringDescriptor.INSTANCE; + } + + @NotNull + @Override + public FileBasedIndex.InputFilter getInputFilter() { + return file -> + file.getFileType() == XmlFileType.INSTANCE + && file.getName().equalsIgnoreCase(ModuleAclXml.FILE_NAME); + } + + @Override + public boolean dependsOnFileContent() { + return true; + } + + @Override + public int getVersion() { + return DomApplicationComponent.getInstance().getCumulativeVersion(false); + } +} diff --git a/testData/inspections/xml/AclResourceXmlInspection/aclResourceWithEmptyIdShouldHaveWarning/acl.xml b/testData/inspections/xml/AclResourceXmlInspection/aclResourceWithEmptyIdShouldHaveWarning/acl.xml new file mode 100644 index 000000000..ac14b819b --- /dev/null +++ b/testData/inspections/xml/AclResourceXmlInspection/aclResourceWithEmptyIdShouldHaveWarning/acl.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/testData/inspections/xml/AclResourceXmlInspection/aclResourceWithNoTitleShouldHaveWarning/acl.xml b/testData/inspections/xml/AclResourceXmlInspection/aclResourceWithNoTitleShouldHaveWarning/acl.xml new file mode 100644 index 000000000..d64e9166b --- /dev/null +++ b/testData/inspections/xml/AclResourceXmlInspection/aclResourceWithNoTitleShouldHaveWarning/acl.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/testData/inspections/xml/AclResourceXmlInspection/overrideAclResourceWithNoTitleShouldNotHaveWarning/acl.xml b/testData/inspections/xml/AclResourceXmlInspection/overrideAclResourceWithNoTitleShouldNotHaveWarning/acl.xml new file mode 100644 index 000000000..18c110102 --- /dev/null +++ b/testData/inspections/xml/AclResourceXmlInspection/overrideAclResourceWithNoTitleShouldNotHaveWarning/acl.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/testData/project/magento2/vendor/magento/module-catalog/etc/acl.xml b/testData/project/magento2/vendor/magento/module-catalog/etc/acl.xml new file mode 100644 index 000000000..abec8f695 --- /dev/null +++ b/testData/project/magento2/vendor/magento/module-catalog/etc/acl.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/com/magento/idea/magento2plugin/inspections/xml/AclResourceXmlInspectionTest.java b/tests/com/magento/idea/magento2plugin/inspections/xml/AclResourceXmlInspectionTest.java new file mode 100644 index 000000000..93fb3ebcb --- /dev/null +++ b/tests/com/magento/idea/magento2plugin/inspections/xml/AclResourceXmlInspectionTest.java @@ -0,0 +1,64 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +package com.magento.idea.magento2plugin.inspections.xml; + +import com.magento.idea.magento2plugin.magento.files.ModuleAclXml; + +public class AclResourceXmlInspectionTest extends InspectionXmlFixtureTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + myFixture.enableInspections(AclResourceXmlInspection.class); + } + + @Override + protected boolean isWriteActionRequired() { + return false; + } + + /** + * ACL resource should have a title. + */ + public void testAclResourceWithNoTitleShouldHaveWarning() { + myFixture.configureByFile(getFixturePath(ModuleAclXml.FILE_NAME)); + + final String errorMessage = inspectionBundle.message( + "inspection.aclResource.error.missingAttribute", + "title" + ); + + assertHasHighlighting(errorMessage); + } + + /** + * Override/Reference for ACL resource may not have a title. + */ + public void testOverrideAclResourceWithNoTitleShouldNotHaveWarning() { + myFixture.configureByFile(getFixturePath(ModuleAclXml.FILE_NAME)); + + final String errorMessage = inspectionBundle.message( + "inspection.aclResource.error.missingAttribute", + "title" + ); + + assertHasNoHighlighting(errorMessage); + } + + /** + * ID attribute of ACL resource should have a value. + */ + public void testAclResourceWithEmptyIdShouldHaveWarning() { + myFixture.configureByFile(getFixturePath(ModuleAclXml.FILE_NAME)); + + final String errorMessage = inspectionBundle.message( + "inspection.aclResource.error.idAttributeCanNotBeEmpty", + "id" + ); + + assertHasHighlighting(errorMessage); + } +}