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);
+ }
+}