diff --git a/platform/core-api/src/com/intellij/model/psi/PsiSymbolReferenceProviderBean.java b/platform/core-api/src/com/intellij/model/psi/PsiSymbolReferenceProviderBean.java index 3587fa5aad1a..2a2dc3f8bb37 100644 --- a/platform/core-api/src/com/intellij/model/psi/PsiSymbolReferenceProviderBean.java +++ b/platform/core-api/src/com/intellij/model/psi/PsiSymbolReferenceProviderBean.java @@ -17,7 +17,7 @@ public final class PsiSymbolReferenceProviderBean extends CustomLoadingExtension * The references will be provided for the specified language and its {@linkplain Language#getBaseLanguage() base languages}. */ @Attribute - @RequiredElement + @RequiredElement(allowEmpty = true) public String hostLanguage; /** diff --git a/platform/extensions/src/com/intellij/openapi/extensions/RequiredElement.java b/platform/extensions/src/com/intellij/openapi/extensions/RequiredElement.java index 6f832fdd0609..0d4bb5845948 100644 --- a/platform/extensions/src/com/intellij/openapi/extensions/RequiredElement.java +++ b/platform/extensions/src/com/intellij/openapi/extensions/RequiredElement.java @@ -1,4 +1,4 @@ -// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.openapi.extensions; import com.intellij.util.xmlb.annotations.Attribute; @@ -16,4 +16,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD}) public @interface RequiredElement { + + /** + * @return {@code true} if the specified property value can be empty. + */ + boolean allowEmpty() default false; } diff --git a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties index bed9bd621ff9..3528b4b2e315 100644 --- a/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties +++ b/plugins/devkit/devkit-core/resources/messages/DevKitBundle.properties @@ -550,6 +550,7 @@ inspections.IncorrectParentDisposableInspection.do.not.use.as.disposable=Don''t inspections.IncorrectParentDisposableInspection.documentation.link.title=Choosing a Disposable Parent extension.point.documentation.field.required.suffix=(required) +extension.point.documentation.field.required.can.be.empty.suffix=(required, empty allowed) extension.point.documentation.implementation.section=Extension Point Implementation gutter.related.option.description=Description diff --git a/plugins/devkit/devkit-core/src/dom/impl/ExtensionDomExtender.java b/plugins/devkit/devkit-core/src/dom/impl/ExtensionDomExtender.java index 059f594ae2c9..15480806144f 100644 --- a/plugins/devkit/devkit-core/src/dom/impl/ExtensionDomExtender.java +++ b/plugins/devkit/devkit-core/src/dom/impl/ExtensionDomExtender.java @@ -77,7 +77,7 @@ public class ExtensionDomExtender extends DomExtender { binding.visit(new ExtensionPointBinding.BindingVisitor() { @Override - public void visitAttribute(@NotNull PsiField field, @NotNull @NonNls String attributeName, boolean required) { + public void visitAttribute(@NotNull PsiField field, @NotNull @NonNls String attributeName, RequiredFlag required) { final With withElement = findWithElement(elements, field); final PsiType fieldType = field.getType(); Class clazz = String.class; @@ -122,7 +122,7 @@ public class ExtensionDomExtender extends DomExtender { } @Override - public void visitTagOrProperty(@NotNull PsiField field, @NotNull String tagName, boolean required) { + public void visitTagOrProperty(@NotNull PsiField field, @NotNull String tagName, RequiredFlag required) { final DomExtension extension = registrar.registerFixedNumberChildExtension(new XmlName(tagName), SimpleTagValue.class) .setDeclaringElement(field); @@ -139,7 +139,7 @@ public class ExtensionDomExtender extends DomExtender { public void visitXCollection(@NotNull PsiField field, @Nullable String tagName, @NotNull PsiAnnotation collectionAnnotation, - boolean required) { + RequiredFlag required) { if (tagName == null) { registerCollectionBinding(field, registrar, collectionAnnotation, required); return; @@ -181,14 +181,19 @@ public class ExtensionDomExtender extends DomExtender { } } - private static void markAsRequired(DomExtension extension, boolean required) { - if (required) extension.addCustomAnnotation(MyRequired.INSTANCE); + private static void markAsRequired(DomExtension extension, ExtensionPointBinding.BindingVisitor.RequiredFlag required) { + if (required == ExtensionPointBinding.BindingVisitor.RequiredFlag.REQUIRED) { + extension.addCustomAnnotation(MyRequired.INSTANCE); + } + else if (required == ExtensionPointBinding.BindingVisitor.RequiredFlag.REQUIRED_ALLOW_EMPTY) { + extension.addCustomAnnotation(MyRequiredCanBeEmpty.INSTANCE); + } } private static void registerCollectionBinding(PsiField field, DomExtensionsRegistrar registrar, PsiAnnotation collectionAnnotation, - boolean required) { + ExtensionPointBinding.BindingVisitor.RequiredFlag required) { final boolean surroundWithTag = PsiUtil.getAnnotationBooleanAttribute(collectionAnnotation, "surroundWithTag"); if (surroundWithTag) return; // todo Set, List, Array @@ -292,6 +297,32 @@ public class ExtensionDomExtender extends DomExtender { } } + @SuppressWarnings("ClassExplicitlyAnnotation") + private static class MyRequiredCanBeEmpty implements Required { + + private static final MyRequiredCanBeEmpty INSTANCE = new MyRequiredCanBeEmpty(); + + @Override + public boolean value() { + return true; + } + + @Override + public boolean nonEmpty() { + return false; + } + + @Override + public boolean identifier() { + return false; + } + + @Override + public Class annotationType() { + return Required.class; + } + } + private static final class MyImplementationExtendClass extends ExtendClassImpl { private final String myInterfaceName; diff --git a/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointBinding.java b/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointBinding.java index ad8f335a5559..659918e48ca7 100644 --- a/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointBinding.java +++ b/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointBinding.java @@ -40,7 +40,13 @@ public class ExtensionPointBinding { final PsiMethod setter = PropertyUtilBase.findSetterForField(field); if ((getter == null || setter == null) && !field.hasModifierProperty(PsiModifier.PUBLIC)) continue; - boolean required = PsiUtil.findAnnotation(RequiredElement.class, field, getter, setter) != null; + final PsiAnnotation requiredAnnotation = PsiUtil.findAnnotation(RequiredElement.class, field, getter, setter); + BindingVisitor.RequiredFlag required = BindingVisitor.RequiredFlag.NOT_REQUIRED; + if (requiredAnnotation != null) { + required = PsiUtil.getAnnotationBooleanAttribute(requiredAnnotation, "allowEmpty") ? + BindingVisitor.RequiredFlag.REQUIRED_ALLOW_EMPTY : BindingVisitor.RequiredFlag.REQUIRED; + } + final PsiAnnotation attributeAnnotation = PsiUtil.findAnnotation(Attribute.class, field, getter, setter); if (attributeAnnotation != null) { String fieldName = PsiUtil.getAnnotationStringAttribute(attributeAnnotation, "value", field.getName()); @@ -75,10 +81,19 @@ public class ExtensionPointBinding { public interface BindingVisitor { - void visitAttribute(@NotNull PsiField field, @NotNull @NonNls String attributeName, boolean required); + enum RequiredFlag { + NOT_REQUIRED, + REQUIRED, + REQUIRED_ALLOW_EMPTY + } - void visitTagOrProperty(@NotNull PsiField field, @NotNull @NonNls String tagName, boolean required); + void visitAttribute(@NotNull PsiField field, @NotNull @NonNls String attributeName, RequiredFlag required); - void visitXCollection(@NotNull PsiField field, @Nullable @NonNls String tagName, @NotNull PsiAnnotation collectionAnnotation, boolean required); + void visitTagOrProperty(@NotNull PsiField field, @NotNull @NonNls String tagName, RequiredFlag required); + + void visitXCollection(@NotNull PsiField field, + @Nullable @NonNls String tagName, + @NotNull PsiAnnotation collectionAnnotation, + RequiredFlag required); } } diff --git a/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointDocumentationProvider.java b/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointDocumentationProvider.java index 6ca7a0300155..94ee9462bf63 100644 --- a/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointDocumentationProvider.java +++ b/plugins/devkit/devkit-core/src/references/extensions/ExtensionPointDocumentationProvider.java @@ -75,12 +75,12 @@ public class ExtensionPointDocumentationProvider implements DocumentationProvide new ExtensionPointBinding(beanClass).visit(new ExtensionPointBinding.BindingVisitor() { @Override - public void visitAttribute(@NotNull PsiField field, @NotNull String attributeName, boolean required) { + public void visitAttribute(@NotNull PsiField field, @NotNull String attributeName, RequiredFlag required) { appendFieldBindingText(field, attributeName, required); } @Override - public void visitTagOrProperty(@NotNull PsiField field, @NotNull String tagName, boolean required) { + public void visitTagOrProperty(@NotNull PsiField field, @NotNull String tagName, RequiredFlag required) { visitAttribute(field, "<" + tagName + ">", required); } @@ -88,15 +88,21 @@ public class ExtensionPointDocumentationProvider implements DocumentationProvide public void visitXCollection(@NotNull PsiField field, @Nullable String tagName, @NotNull PsiAnnotation collectionAnnotation, - boolean required) { + RequiredFlag required) { visitAttribute(field, "<" + tagName + ">...", required); } - private void appendFieldBindingText(@NotNull PsiField field, @NotNull @NlsSafe String displayName, boolean required) { + private void appendFieldBindingText(@NotNull PsiField field, @NotNull @NlsSafe String displayName, RequiredFlag required) { HtmlChunk hyperLink = createLink(JavaDocUtil.getReferenceText(field.getProject(), field), displayName); final String typeText = field.getType().getPresentableText(); - final String requiredText = required ? " " + DevKitBundle.message("extension.point.documentation.field.required.suffix") : ""; + String requiredText = ""; + if (required == RequiredFlag.REQUIRED) { + requiredText = " " + DevKitBundle.message("extension.point.documentation.field.required.suffix"); + } + else if (required == RequiredFlag.REQUIRED_ALLOW_EMPTY) { + requiredText = " " + DevKitBundle.message("extension.point.documentation.field.required.can.be.empty.suffix"); + } final String initializer = field.getInitializer() != null ? " = " + field.getInitializer().getText() : ""; bindingRows.append(createSectionRow(hyperLink, typeText + requiredText + initializer)); } diff --git a/plugins/devkit/devkit-java-tests/testData/codeInsight/ExtensionsHighlighting.xml b/plugins/devkit/devkit-java-tests/testData/codeInsight/ExtensionsHighlighting.xml index c659e17d6a8c..156fc33f4730 100644 --- a/plugins/devkit/devkit-java-tests/testData/codeInsight/ExtensionsHighlighting.xml +++ b/plugins/devkit/devkit-java-tests/testData/codeInsight/ExtensionsHighlighting.xml @@ -46,6 +46,9 @@ forClass="INVALID_VALUE" /> + + INVALID_VALUE"/> diff --git a/plugins/devkit/devkit-java-tests/testData/references/extensions/bar/MyExtensionPoint.java b/plugins/devkit/devkit-java-tests/testData/references/extensions/bar/MyExtensionPoint.java index ee998ffaf5bc..086294658b8d 100644 --- a/plugins/devkit/devkit-java-tests/testData/references/extensions/bar/MyExtensionPoint.java +++ b/plugins/devkit/devkit-java-tests/testData/references/extensions/bar/MyExtensionPoint.java @@ -13,6 +13,10 @@ public class MyExtensionPoint { @Attribute public String implementationClass; + @RequiredElement(allowEmpty = true) + @Attribute + public String stringCanBeEmpty; + @Tag public Integer intValue; } \ No newline at end of file diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/codeInsight/PluginXmlFunctionalTest.groovy b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/codeInsight/PluginXmlFunctionalTest.groovy index ed4e98510941..e1c5ad9187de 100644 --- a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/codeInsight/PluginXmlFunctionalTest.groovy +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/codeInsight/PluginXmlFunctionalTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + * Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. */ package org.jetbrains.idea.devkit.codeInsight @@ -143,11 +143,13 @@ class PluginXmlFunctionalTest extends JavaCodeInsightFixtureTestCase { "}") myFixture.addClass("package foo; " + "import com.intellij.util.xmlb.annotations.Attribute; " + + "import com.intellij.openapi.extensions.RequiredElement; " + "public class MyServiceDescriptor { " + " @Attribute public String serviceImplementation; " + " @Attribute public java.util.concurrent.TimeUnit timeUnit; " + " @Attribute public java.lang.Integer integerNullable; " + " @Attribute public int intPropertyForClass; " + + " @Attribute @RequiredElement(allowEmpty=true) public String canBeEmptyString; " + " @Attribute public boolean forClass; " + "}") diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/references/extensions/ExtensionPointDocumentationProviderTest.java b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/references/extensions/ExtensionPointDocumentationProviderTest.java index dfbd87b09327..7cf643f16f1a 100644 --- a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/references/extensions/ExtensionPointDocumentationProviderTest.java +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/references/extensions/ExtensionPointDocumentationProviderTest.java @@ -52,7 +52,7 @@ public class ExtensionPointDocumentationProviderTest extends LightJavaCodeInsigh "
bar
public class MyExtensionPoint\n" + "extends Object
\n" + " MyExtensionPoint JavaDoc.\n" + - "


implementationClass

String (required)

<intValue>

Integer

Extension Point Implementation

bar
public interface MyExtension
\n" + + "


implementationClass

String (required)

stringCanBeEmpty

String (required, empty allowed)

<intValue>

Integer

Extension Point Implementation

bar
public interface MyExtension
\n" + " My Extension Javadoc.\n" + "

", provider.generateDoc(docElement, getOriginalElement()));