// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.devkit.inspections;

import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.MoveToPackageFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.codeInspection.util.InspectionMessage;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.diagnostic.ITNReporter;
import com.intellij.ide.plugins.PluginManager;
import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.diagnostic.ErrorReportSubmitter;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.LoadingOrder;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.IntelliJProjectUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.BuildNumber;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.NavigatableAdapter;
import com.intellij.psi.*;
import com.intellij.psi.impl.light.LightMethodBuilder;
import com.intellij.psi.util.PsiFormatUtil;
import com.intellij.psi.util.PsiFormatUtilBase;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.xml.*;
import com.intellij.util.xml.highlighting.*;
import com.intellij.util.xml.reflect.DomAttributeChildDescription;
import com.intellij.xml.CommonXmlStrings;
import com.intellij.xml.util.IncludedXmlTag;
import org.jetbrains.annotations.*;
import org.jetbrains.idea.devkit.DevKitBundle;
import org.jetbrains.idea.devkit.dom.*;
import org.jetbrains.idea.devkit.dom.Listeners.Listener;
import org.jetbrains.idea.devkit.dom.impl.PluginPsiClassConverter;
import org.jetbrains.idea.devkit.dom.index.ExtensionPointIndex;
import org.jetbrains.idea.devkit.inspections.quickfix.AddWithTagFix;
import org.jetbrains.idea.devkit.module.PluginModuleType;
import org.jetbrains.idea.devkit.util.DescriptorUtil;
import org.jetbrains.idea.devkit.util.PluginPlatformInfo;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.intellij.psi.search.GlobalSearchScope.projectScope;
import static org.jetbrains.idea.devkit.module.IdePluginModuleBuilderKt.DEVKIT_NEWLY_GENERATED_PROJECT;

@VisibleForTesting
@ApiStatus.Internal
public final class PluginXmlDomInspection extends DevKitPluginXmlInspectionBase {

  private static final int MINIMAL_DESCRIPTION_LENGTH = 40;

  public static final @NonNls String DEPENDENCIES_DOC_URL =
    "https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html?from=DevkitPluginXmlInspection";

  private static class Holder {
    private static final Pattern BASE_LINE_EXTRACTOR = Pattern.compile("(?:\\p{javaLetter}+-)?(\\d+)(?:\\..*)?");
  }

  private static final int FIRST_BRANCH_SUPPORTING_STAR = 131;

  public boolean myIgnoreUnstableApiDeclaredInThisProject = false;

  @Override
  public @NotNull String getShortName() {
    return "PluginXmlValidity";
  }

  @Override
  public @NotNull OptPane getOptionsPane() {
    return OptPane.pane(
      OptPane.checkbox("myIgnoreUnstableApiDeclaredInThisProject",
                       DevKitBundle.message("devkit.unstable.api.usage.ignore.declared.inside.this.project"))
    );
  }

  @Override
  protected void checkDomElement(@NotNull DomElement element,
                                 @NotNull DomElementAnnotationHolder holder,
                                 @NotNull DomHighlightingHelper helper) {
    if (!isAllowed(holder)) return;

    super.checkDomElement(element, holder, helper);

    if (element instanceof IdeaPlugin) {
      Module module = element.getModule();
      if (module != null) {
        annotateIdeaPlugin(module.getProject(), (IdeaPlugin)element, holder, module);
        checkJetBrainsPlugin((IdeaPlugin)element, holder, module);
      }
    }
    else if (element instanceof Extension) {
      annotateExtension((Extension)element, holder);
    }
    else if (element instanceof ExtensionPoint) {
      annotateExtensionPoint((ExtensionPoint)element, holder);
    }
    else if (element instanceof Vendor) {
      annotateVendor(element.getManager().getProject(), (Vendor)element, holder);
    }
    else if (element instanceof ProductDescriptor) {
      annotateProductDescriptor((ProductDescriptor)element, holder);
    }
    else if (element instanceof IdeaVersion) {
      annotateIdeaVersion((IdeaVersion)element, holder);
    }
    else if (element instanceof Dependency) {
      annotateDependency((Dependency)element, holder);
    }
    else if (element instanceof DependencyDescriptor) {
      annotateDependencyDescriptor((DependencyDescriptor)element, holder);
    }
    else if (element instanceof ContentDescriptor) {
      annotateContentDescriptor((ContentDescriptor)element, holder);
    }
    else if (element instanceof Extensions) {
      annotateExtensions((Extensions)element, holder);
    }
    else if (element instanceof Extensions.UnresolvedExtension) {
      annotateUnresolvedExtension((Extensions.UnresolvedExtension)element, holder);
    }
    else if (element instanceof AddToGroup) {
      annotateAddToGroup((AddToGroup)element, holder);
    }
    else if (element instanceof Action) {
      annotateAction((Action)element, holder);
    }
    else if (element instanceof Reference) {
      annotateReference((Reference)element, holder);
    }
    else if (element instanceof Synonym) {
      annotateSynonym((Synonym)element, holder);
    }
    else if (element instanceof Group) {
      annotateGroup((Group)element, holder);
    }
    else if (element instanceof Component) {
      annotateComponent((Component)element, holder);
      if (element instanceof Component.Project) {
        annotateProjectComponent((Component.Project)element, holder);
      }
    }
    else //noinspection deprecation
      if (element instanceof Helpset) {
        highlightRedundant(element, DevKitBundle.message("inspections.plugin.xml.deprecated.helpset"), holder);
      }
      else if (element instanceof Listeners) {
        annotateListeners((Listeners)element, holder);
      }
      else if (element instanceof Listener) {
        annotateListener((Listener)element, holder);
      }

    if (element instanceof GenericDomValue<?> domValue) {
      if (domValue.getConverter() instanceof PluginPsiClassConverter) {
        @SuppressWarnings("unchecked") GenericDomValue<PsiClass> psiClassDomValue = (GenericDomValue<PsiClass>)element;
        annotatePsiClassValue(psiClassDomValue, holder);
      }
    }
  }

  private static void annotateDependencyDescriptor(DependencyDescriptor descriptor, DomElementAnnotationHolder holder) {
    if (descriptor.getModuleEntry().isEmpty() &&
        descriptor.getPlugin().isEmpty()) {
      holder.createProblem(descriptor, HighlightSeverity.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.dependency.descriptor.at.least.one.dependency"));
      return;
    }

    final IdeaPlugin ideaPlugin = descriptor.getParentOfType(IdeaPlugin.class, false);
    assert ideaPlugin != null;
    for (Dependency dependency : ideaPlugin.getDepends()) {
      if (dependency.getOptional().getValue() == Boolean.TRUE) continue;
      holder.createProblem(dependency, HighlightSeverity.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.dependency.descriptor.cannot.use.depends")).highlightWholeElement();
    }
  }

  private static void annotateContentDescriptor(ContentDescriptor descriptor, DomElementAnnotationHolder holder) {
    if (descriptor.getModuleEntry().isEmpty()) {
      holder.createProblem(descriptor, HighlightSeverity.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.module.descriptor.at.least.one.dependency"));
    }
  }

  private static void annotatePsiClassValue(GenericDomValue<PsiClass> psiClassDomValue, DomElementAnnotationHolder holder) {
    final PsiClass psiClass = psiClassDomValue.getValue();
    if (psiClass == null) return;
    if (psiClass.getContainingClass() != null &&
        !StringUtil.containsChar(StringUtil.notNullize(psiClassDomValue.getRawText()), '$')) {
      holder.createProblem(psiClassDomValue, DevKitBundle.message("inspections.plugin.xml.inner.class.must.be.separated.with.dollar"));
    }

    Module module = psiClassDomValue.getModule();
    if (module == null) return;

    IdeaPlugin ideaPlugin = psiClassDomValue.getParentOfType(IdeaPlugin.class, true);
    assert ideaPlugin != null;
    String pluginPackage = ideaPlugin.getPackage().getStringValue();
    if (pluginPackage == null) return;

    final String psiClassFqn = psiClass.getQualifiedName();
    assert psiClassFqn != null;

    // only highlight if located in the same module
    if (!StringUtil.startsWith(psiClassFqn, pluginPackage + ".") &&
        module == ModuleUtilCore.findModuleForPsiElement(psiClass)) {
      holder.createProblem(psiClassDomValue, HighlightSeverity.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.dependency.class.located.in.wrong.package",
                                                psiClassFqn, pluginPackage),
                           new MoveToPackageFix(psiClass.getContainingFile(), pluginPackage));
    }
  }

  private static void annotateListener(Listener listener, DomElementAnnotationHolder holder) {
    final PsiClass listenerClass = listener.getListenerClassName().getValue();
    final PsiClass topicClass = listener.getTopicClassName().getValue();
    if (listenerClass == null || topicClass == null) return;

    if (!listenerClass.isInheritor(topicClass, true)) {
      holder.createProblem(listener.getListenerClassName(),
                           DevKitBundle.message("inspections.plugin.xml.listener.does.not.inherit",
                                                listener.getListenerClassName().getStringValue(),
                                                listener.getTopicClassName().getStringValue()));
    }
  }

  private static final int LISTENERS_PLATFORM_VERSION = 193;
  private static final int LISTENERS_OS_ATTRIBUTE_PLATFORM_VERSION = 201;

  private static void annotateListeners(Listeners listeners, DomElementAnnotationHolder holder) {
    final Module module = listeners.getModule();
    if (module == null || IntelliJProjectUtil.isIntelliJPlatformProject(module.getProject())) return;

    PluginPlatformInfo platformInfo = PluginPlatformInfo.forDomElement(listeners);
    final PluginPlatformInfo.PlatformResolveStatus resolveStatus = platformInfo.getResolveStatus();

    if (resolveStatus == PluginPlatformInfo.PlatformResolveStatus.DEVKIT_NO_MAIN) {
      holder.createProblem(listeners, ProblemHighlightType.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.since.build.could.not.locate.main.descriptor"), null)
        .highlightWholeElement();
      return;
    }

    if (resolveStatus == PluginPlatformInfo.PlatformResolveStatus.DEVKIT_NO_SINCE_BUILD) {
      final boolean noSinceBuildXml = !DomUtil.hasXml(platformInfo.getMainIdeaPlugin().getIdeaVersion().getSinceBuild());
      LocalQuickFix[] fixes =
        noSinceBuildXml ? new LocalQuickFix[]{new AddDomElementQuickFix<>(platformInfo.getMainIdeaPlugin().getIdeaVersion())} :
        LocalQuickFix.EMPTY_ARRAY;
      holder.createProblem(listeners, ProblemHighlightType.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.since.build.must.be.specified"), null,
                           fixes)
        .highlightWholeElement();
      return;
    }

    final BuildNumber buildNumber = platformInfo.getSinceBuildNumber();
    if (buildNumber == null) {
      holder.createProblem(listeners, ProblemHighlightType.ERROR,
                           DevKitBundle.message("inspections.plugin.xml.since.build.could.not.determine.platform.version"), null)
        .highlightWholeElement();
      return;
    }

    final int baselineVersion = buildNumber.getBaselineVersion();

    boolean canHaveOsAttribute = baselineVersion >= LISTENERS_OS_ATTRIBUTE_PLATFORM_VERSION;
    if (!canHaveOsAttribute) {
      for (Listener listener : listeners.getListeners()) {
        if (DomUtil.hasXml(listener.getOs())) {
          holder.createProblem(listener.getOs(), ProblemHighlightType.ERROR,
                               DevKitBundle.message("inspections.plugin.xml.since.build.listeners.os.attribute",
                                                    LISTENERS_OS_ATTRIBUTE_PLATFORM_VERSION,
                                                    baselineVersion),
                               null)
            .highlightWholeElement();
        }
      }
    }


    if (baselineVersion >= LISTENERS_PLATFORM_VERSION) return;

    holder.createProblem(listeners, ProblemHighlightType.ERROR,
                         DevKitBundle.message("inspections.plugin.xml.since.build.listeners.not.available",
                                              LISTENERS_PLATFORM_VERSION,
                                              baselineVersion),
                         null)
      .highlightWholeElement();
  }

  private static void annotateDependency(Dependency dependency, DomElementAnnotationHolder holder) {
    final GenericAttributeValue<Boolean> optional = dependency.getOptional();
    if (optional.getValue() == Boolean.FALSE) {
      highlightRedundant(optional,
                         DevKitBundle.message("inspections.plugin.xml.dependency.superfluous.optional"),
                         ProblemHighlightType.WARNING, holder);
    }
    else if (optional.getValue() == Boolean.TRUE &&
             !DomUtil.hasXml(dependency.getConfigFile())) {
      holder.createProblem(dependency, DevKitBundle.message("inspections.plugin.xml.dependency.specify.config.file"),
                           new AddDomElementQuickFix<>(dependency.getConfigFile())).highlightWholeElement();
    }
  }

  private static void annotateIdeaPlugin(Project project, IdeaPlugin ideaPlugin, DomElementAnnotationHolder holder, @NotNull Module module) {
    //noinspection deprecation
    highlightAttributeNotUsedAnymore(ideaPlugin.getIdeaPluginVersion(), holder);
    //noinspection deprecation
    if (DomUtil.hasXml(ideaPlugin.getUseIdeaClassloader())) {
      //noinspection deprecation
      highlightDeprecated(ideaPlugin.getUseIdeaClassloader(), DevKitBundle.message("inspections.plugin.xml.deprecated"), holder, true, true);
    }

    //noinspection deprecation
    if (DomUtil.hasXml(ideaPlugin.getImplementationDetail())) {
      //noinspection deprecation
      highlightDeprecated(ideaPlugin.getImplementationDetail(), DevKitBundle.message("inspections.plugin.xml.deprecated.implementation.detail"),
                          holder, true, true);
    }

    //noinspection deprecation
    if (DomUtil.hasXml(ideaPlugin.getPackage())) {
      //noinspection deprecation
      highlightDeprecated(ideaPlugin.getPackage(), DevKitBundle.message("inspections.plugin.xml.deprecated.package.attribute"), holder, true, true);
    }

    checkMaxLength(ideaPlugin.getUrl(), 255, holder);
    checkValidWebsite(ideaPlugin.getUrl(), holder);

    checkMaxLength(ideaPlugin.getId(), 255, holder);

    checkTemplateText(project, ideaPlugin.getName(), "Plugin display name here", holder);
    checkTemplateTextContainsWord(project, ideaPlugin.getName(), holder, "plugin", "support", "integration", "tool", "tools", "IntelliJ", "JetBrains");
    checkMaxLength(ideaPlugin.getName(), 255, holder);

    checkMaxLength(ideaPlugin.getDescription(), 65535, holder);
    checkHasRealText(ideaPlugin.getDescription(), holder);
    checkTemplateTextContains(project, ideaPlugin.getDescription(), "Enter short description for your plugin here.", holder);
    checkTemplateTextContains(project, ideaPlugin.getDescription(), "most HTML tags may be used", holder);

    checkMaxLength(ideaPlugin.getChangeNotes(), 65535, holder);
    checkTemplateTextContains(project, ideaPlugin.getChangeNotes(), "Add change notes here", holder);
    checkTemplateTextContains(project, ideaPlugin.getChangeNotes(), "most HTML tags may be used", holder);

    if (!ideaPlugin.hasRealPluginId()) return;

    MultiMap<String, Dependency> dependencies = MultiMap.create();
    ideaPlugin.getDepends().forEach(dependency -> {
      if (DomUtil.hasXml(dependency.getConfigFile())) {
        dependencies.putValue(dependency.getConfigFile().getStringValue(), dependency);
      }
    });
    for (Map.Entry<String, Collection<Dependency>> entry : dependencies.entrySet()) {
      if (entry.getValue().size() > 1) {
        for (Dependency dependency : entry.getValue()) {
          if (dependency.getXmlTag() instanceof IncludedXmlTag) continue;
          highlightRedundant(dependency, DevKitBundle.message("inspections.plugin.xml.duplicated.dependency", entry.getKey()),
                             ProblemHighlightType.ERROR, holder);
        }
      }
    }

    boolean isNotIdeaProject = !IntelliJProjectUtil.isIntelliJPlatformProject(module.getProject());

    if (isNotIdeaProject &&
        !DomUtil.hasXml(ideaPlugin.getVersion()) &&
        PluginModuleType.isOfType(module)) {
      holder.createProblem(ideaPlugin, DevKitBundle.message("inspections.plugin.xml.version.must.be.specified"),
                           new AddMissingMainTag(DevKitBundle.message("inspections.plugin.xml.add.version.tag"), ideaPlugin.getVersion(),
                                                 ""));
    }
    checkMaxLength(ideaPlugin.getVersion(), 64, holder);


    if (isNotIdeaProject && !DomUtil.hasXml(ideaPlugin.getVendor())) {
      holder.createProblem(ideaPlugin, DevKitBundle.message("inspections.plugin.xml.vendor.must.be.specified"),
                           new AddMissingMainTag(DevKitBundle.message("inspections.plugin.xml.add.vendor.tag"), ideaPlugin.getVendor(),
                                                 ""));
    }
  }

  private static void checkJetBrainsPlugin(IdeaPlugin ideaPlugin, DomElementAnnotationHolder holder, @NotNull Module module) {
    if (!IntelliJProjectUtil.isIntelliJPlatformProject(module.getProject())) return;

    if (DomUtil.hasXml(ideaPlugin.getUrl())) {
      String url = ideaPlugin.getUrl().getStringValue();
      if ("https://www.jetbrains.com/idea".equals(url)) {
        highlightRedundant(ideaPlugin.getUrl(),
                           DevKitBundle.message("inspections.plugin.xml.plugin.jetbrains.no.generic.plugin.url"), holder);
      }
    }

    if (!ideaPlugin.hasRealPluginId()) return;

    @NonNls String id = ideaPlugin.getId().getStringValue();
    if (id != null &&
        (StringUtil.startsWith(id, "com.android.") || //NON-NLS
         id.equals("org.jetbrains.android"))) {
      return;
    }

    if (!isUnderProductionSources(ideaPlugin, module)) return;

    final Vendor vendor = ideaPlugin.getVendor();
    if (!DomUtil.hasXml(vendor)) {
      holder.createProblem(DomUtil.getFileElement(ideaPlugin),
                           DevKitBundle.message("inspections.plugin.xml.plugin.should.have.jetbrains.vendor"),
                           new AddMissingMainTag(DevKitBundle.message("inspections.plugin.xml.vendor.specify.jetbrains"),
                                                 vendor, PluginManagerCore.VENDOR_JETBRAINS));
    }
    else if (!PluginManagerCore.isVendorJetBrains(vendor.getValue())) {
      holder.createProblem(vendor, DevKitBundle.message("inspections.plugin.xml.plugin.should.have.jetbrains.vendor"));
    }
    else {
      final String url = vendor.getUrl().getStringValue();
      if (url != null && StringUtil.endsWith(url, "jetbrains.com")) {
        highlightRedundant(vendor.getUrl(),
                           DevKitBundle.message("inspections.plugin.xml.plugin.jetbrains.vendor.no.url", url), holder);
      }
    }

    if (DomUtil.hasXml(vendor.getEmail())) {
      highlightRedundant(vendor.getEmail(),
                         DevKitBundle.message("inspections.plugin.xml.plugin.jetbrains.vendor.no.email"), holder);
    }
    if (DomUtil.hasXml(ideaPlugin.getVersion())) {
      highlightRedundant(ideaPlugin.getVersion(),
                         DevKitBundle.message("inspections.plugin.xml.plugin.jetbrains.no.version"), holder);
    }
    if (DomUtil.hasXml(ideaPlugin.getIdeaVersion())) {
      highlightRedundant(ideaPlugin.getIdeaVersion(),
                         DevKitBundle.message("inspections.plugin.xml.plugin.jetbrains.no.idea.version"), holder);
    }
  }

  private static void annotateExtensionPoint(ExtensionPoint extensionPoint,
                                             DomElementAnnotationHolder holder) {
    if (extensionPoint.getWithElements().isEmpty() &&
        !extensionPoint.collectMissingWithTags().isEmpty()) {
      holder.createProblem(extensionPoint, DevKitBundle.message("inspections.plugin.xml.ep.doesnt.have.with"), new AddWithTagFix());
    }

    checkEpBeanClassAndInterface(extensionPoint, holder);
    checkEpNameAndQualifiedName(extensionPoint, holder);

    if (DomUtil.hasXml(extensionPoint.getQualifiedName())) {
      IdeaPlugin ideaPlugin = DomUtil.getParentOfType(extensionPoint, IdeaPlugin.class, true);
      assert ideaPlugin != null;
      final String pluginId = ideaPlugin.getPluginId();
      if (pluginId != null) {
        final String epQualifiedName = extensionPoint.getQualifiedName().getStringValue();
        if (epQualifiedName != null && epQualifiedName.startsWith(pluginId + ".")) {
          holder.createProblem(extensionPoint.getQualifiedName(), ProblemHighlightType.WARNING,
                               DevKitBundle.message("inspections.plugin.xml.ep.qualifiedName.superfluous"), null,
                               new LocalQuickFix() {
                                 @Override
                                 public @IntentionFamilyName @NotNull String getFamilyName() {
                                   return DevKitBundle.message("inspections.plugin.xml.ep.qualifiedName.superfluous.fix");
                                 }

                                 @Override
                                 public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
                                   ExtensionPoint fixExtensionPoint =
                                     DomUtil.findDomElement(descriptor.getPsiElement(), ExtensionPoint.class);
                                   if (fixExtensionPoint == null) return;

                                   fixExtensionPoint.getQualifiedName().undefine();
                                   fixExtensionPoint.getName().setStringValue(StringUtil.substringAfter(epQualifiedName, pluginId + "."));
                                 }
                               }).highlightWholeElement();
        }
      }
    }
  }

  private static void checkEpBeanClassAndInterface(ExtensionPoint extensionPoint, DomElementAnnotationHolder holder) {
    boolean hasBeanClass = DomUtil.hasXml(extensionPoint.getBeanClass());
    boolean hasInterface = DomUtil.hasXml(extensionPoint.getInterface());
    if (hasBeanClass && hasInterface) {
      highlightRedundant(extensionPoint.getBeanClass(),
                         DevKitBundle.message("inspections.plugin.xml.ep.both.beanClass.and.interface"),
                         ProblemHighlightType.GENERIC_ERROR, holder);
      highlightRedundant(extensionPoint.getInterface(),
                         DevKitBundle.message("inspections.plugin.xml.ep.both.beanClass.and.interface"),
                         ProblemHighlightType.GENERIC_ERROR, holder);
    }
    else if (!hasBeanClass && !hasInterface) {
      holder.createProblem(extensionPoint, ProblemHighlightType.GENERIC_ERROR,
                           DevKitBundle.message("inspections.plugin.xml.ep.missing.beanClass.and.interface"), null,
                           new AddDomElementQuickFix<>(extensionPoint.getBeanClass()),
                           new AddDomElementQuickFix<>(extensionPoint.getInterface()));
    }
  }


  private static void checkEpNameAndQualifiedName(ExtensionPoint extensionPoint, DomElementAnnotationHolder holder) {
    GenericAttributeValue<String> name = extensionPoint.getName();
    GenericAttributeValue<String> qualifiedName = extensionPoint.getQualifiedName();
    boolean hasName = DomUtil.hasXml(name);
    boolean hasQualifiedName = DomUtil.hasXml(qualifiedName);

    if (hasName && hasQualifiedName) {
      holder.createProblem(extensionPoint, ProblemHighlightType.GENERIC_ERROR,
                           DevKitBundle.message("inspections.plugin.xml.ep.both.name.and.qualifiedName"), null);
    }
    else if (!hasName && !hasQualifiedName) {
      holder.createProblem(extensionPoint, ProblemHighlightType.GENERIC_ERROR,
                           DevKitBundle.message("inspections.plugin.xml.ep.missing.name.and.qualifiedName"), null);
    }

    if (hasQualifiedName) {
      if (!isValidEpName(qualifiedName)) {
        String message = DevKitBundle.message("inspections.plugin.xml.invalid.ep.name.description",
                                              DevKitBundle.message("inspections.plugin.xml.invalid.ep.qualifiedName"),
                                              qualifiedName.getValue());
        holder.createProblem(qualifiedName, ProblemHighlightType.WEAK_WARNING, message, null);
      }
      return;
    }

    if (hasName && !isValidEpName(name)) {
      String message = DevKitBundle.message("inspections.plugin.xml.invalid.ep.name.description",
                                            DevKitBundle.message("inspections.plugin.xml.invalid.ep.name"),
                                            name.getValue());
      holder.createProblem(name, ProblemHighlightType.WEAK_WARNING, message, null);
    }
  }

  private static boolean isValidEpName(GenericAttributeValue<String> nameAttrValue) {
    if (!nameAttrValue.exists()) {
      return true;
    }
    @NonNls String name = nameAttrValue.getValue();

    // skip some known offenders in the IJ project
    if (name != null
        && (StringUtil.startsWith(name, "Pythonid.") ||
            StringUtil.startsWith(name, "DevKit.") ||
            StringUtil.startsWith(name, "Git4Idea."))) {
      if (IntelliJProjectUtil.isIntelliJPlatformProject(nameAttrValue.getManager().getProject())) {
        return true;
      }
    }

    if (StringUtil.isEmpty(name) ||
        !Character.isLowerCase(name.charAt(0)) || // also checks that the name doesn't start with a dot
        StringUtil.toUpperCase(name).equals(name) || // not all uppercase chars
        !StringUtil.isLatinAlphanumeric(name.replace(".", "")) ||
        name.charAt(name.length() - 1) == '.') {
      return false;
    }

    List<String> fragments = StringUtil.split(name, ".");
    if (ContainerUtil.exists(fragments, f -> Character.isUpperCase(f.charAt(0)))) {
      return false;
    }

    String epName = fragments.getLast();
    List<String> butLast = fragments.subList(0, fragments.size() - 1);
    List<String> words = StringUtil.getWordsIn(epName);
    return !ContainerUtil.exists(words, w -> ContainerUtil.exists(butLast, f -> StringUtil.equalsIgnoreCase(w, f)));
  }

  private static void annotateExtensions(Extensions extensions, DomElementAnnotationHolder holder) {
    final GenericAttributeValue<IdeaPlugin> xmlnsAttribute = extensions.getXmlns();
    if (DomUtil.hasXml(xmlnsAttribute)) {
      highlightDeprecated(xmlnsAttribute, DevKitBundle.message("inspections.plugin.xml.use.defaultExtensionNs"), holder, false, true);
      return;
    }

    if (!DomUtil.hasXml(extensions.getDefaultExtensionNs())) {
      holder.createProblem(
        extensions,
        ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
        DevKitBundle.message("inspections.plugin.xml.specify.defaultExtensionNs.explicitly", Extensions.DEFAULT_PREFIX),
        null,
        new AddDomElementQuickFix<>(extensions.getDefaultExtensionNs()) {
          @Override
          public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
            super.applyFix(project, descriptor);
            myElement.setStringValue(Extensions.DEFAULT_PREFIX);
          }
        });
    }
  }

  private static void annotateUnresolvedExtension(Extensions.UnresolvedExtension unresolvedExtension, DomElementAnnotationHolder holder) {
    final Module module = unresolvedExtension.getModule();
    if (module == null) return;

    final Extensions extensions = unresolvedExtension.getParentOfType(Extensions.class, true);
    assert extensions != null;
    String qualifiedExtensionId = extensions.getEpPrefix() + unresolvedExtension.getXmlElementName();

    final ExtensionPoint extensionPoint = ExtensionPointIndex.findExtensionPoint(module, qualifiedExtensionId);
    if (extensionPoint == null) {
      String message = new HtmlBuilder()
        .append(DevKitBundle.message("error.cannot.resolve.extension.point", qualifiedExtensionId))
        .nbsp()
        .append(HtmlChunk.link(DEPENDENCIES_DOC_URL, DevKitBundle.message("error.cannot.resolve.plugin.reference.link.title")))
        .wrapWith(HtmlChunk.html()).toString();

      holder.createProblem(unresolvedExtension, ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, message, null);
      return;
    }

    final IdeaPlugin ideaPlugin = extensionPoint.getParentOfType(IdeaPlugin.class, true);
    assert ideaPlugin != null;
    String dependencyId = ideaPlugin.getPluginId();

    final LocalQuickFix addDependsFix = new LocalQuickFix() {
      @Override
      public @IntentionFamilyName @NotNull String getFamilyName() {
        return DevKitBundle.message("error.cannot.resolve.extension.point.missing.dependency.fix.family.name");
      }

      @Override
      public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
        DomElement domElement = DomUtil.getDomElement(descriptor.getPsiElement());
        if (domElement == null) return;

        final IdeaPlugin ideaPlugin = domElement.getParentOfType(IdeaPlugin.class, true);
        assert ideaPlugin != null;
        final Dependency dependency = ideaPlugin.addDependency();
        dependency.setStringValue(dependencyId);
      }
    };
    holder.createProblem(unresolvedExtension,
                         DevKitBundle.message("error.cannot.resolve.extension.point.missing.dependency", qualifiedExtensionId),
                         addDependsFix);
  }

  private static void annotateIdeaVersion(IdeaVersion ideaVersion, DomElementAnnotationHolder holder) {
    //noinspection deprecation
    highlightAttributeNotUsedAnymore(ideaVersion.getMin(), holder);
    //noinspection deprecation
    highlightAttributeNotUsedAnymore(ideaVersion.getMax(), holder);
    highlightUntilBuild(ideaVersion, ideaVersion.getUntilBuild(), "until-build", holder);
    highlightUntilBuild(ideaVersion, ideaVersion.getStrictUntilBuild(), "strict-until-build", holder);

    GenericAttributeValue<BuildNumber> sinceBuild = ideaVersion.getSinceBuild();
    if (DomUtil.hasXml(sinceBuild)) {
      BuildNumber sinceBuildNumber = sinceBuild.getValue();
      if (sinceBuildNumber != null) {
        checkUntilBuildGreaterThanSinceBuild(ideaVersion.getUntilBuild(), "until-build", sinceBuildNumber, holder);
        checkUntilBuildGreaterThanSinceBuild(ideaVersion.getStrictUntilBuild(), "strict-until-build", sinceBuildNumber, holder);
      }
    }
  }

  private static void checkUntilBuildGreaterThanSinceBuild(GenericAttributeValue<BuildNumber> untilBuild,
                                                           @NonNls String untilAttributeName, BuildNumber sinceBuildNumber,
                                                           DomElementAnnotationHolder holder) {
    if (DomUtil.hasXml(untilBuild)) {
      BuildNumber untilBuildNumber = untilBuild.getValue();
      if (untilBuildNumber != null) {
        int compare = Comparing.compare(sinceBuildNumber, untilBuildNumber);
        if (compare > 0) {
