From 936c2c4625ffc983ca71d9d9e7dba095327b7d48 Mon Sep 17 00:00:00 2001 From: Alexander Bubenchikov Date: Sat, 25 Oct 2025 08:37:47 +0000 Subject: [PATCH] [maven] [IDEA-381098] maven4 quick fixes Merge-request: IJ-MR-175311 Merged-by: Aleksandr Bubenchikov (cherry picked from commit e3cd28795c7a24db59fd581d93843f72bfea3278) IJ-CR-180089 GitOrigin-RevId: 1147b8e6ee9d02eb7f43d5f89787bcbc41712fff --- .../idea/maven/model/MavenConstants.java | 18 +- .../buildtool/quickfix/UpdateXmlsTo410.kt | 45 ++- .../jetbrains/idea/maven/dom/MavenDomUtil.kt | 26 ++ .../dom/MavenModelDocumentationProvider.java | 19 +- .../converters/MavenModelVersionConverter.kt | 7 +- .../converters/MavenPackagingConverter.java | 33 -- .../dom/converters/MavenPackagingConverter.kt | 49 +++ .../MavenModelVersionInlayHintProvider.kt | 40 +++ .../AbstractMavenRedundantParentInspection.kt | 75 +++++ ...en4RedundantParentCoordinatesInspection.kt | 88 ++++++ .../MavenBomPackagingInOldSchema.kt | 45 +++ .../MavenModulesInMaven4Inspection.kt | 80 +++++ ...avenNewModelVersionInOldMavenInspection.kt | 174 +++++++++++ .../MavenNoRootDefinedInspection.kt | 97 ++++++ .../MavenRedundantGroupIdInspection.java | 76 ----- .../MavenRedundantGroupIdInspection.kt | 22 ++ .../MavenRedundantVersionInspection.kt | 23 ++ .../references/MavenPropertyPsiReference.java | 34 ++- .../MavenModelVersionSynchronizerService.kt | 118 ++++++++ .../MavenModelVersionSyncronizerImpl.kt | 284 ++++++++++++++++++ .../idea/maven/project/MavenProject.kt | 9 - .../maven/project/MavenProjectsManager.java | 17 +- .../idea/maven/server/MavenDistribution.kt | 3 +- .../maven/server/MavenDistributionsCache.java | 37 ++- .../maven/server/MavenWrapperDownloader.java | 7 +- .../MavenWrapperEventLogNotification.kt | 8 +- .../idea/maven/server/MavenWrapperSupport.kt | 28 +- .../jetbrains/idea/maven/utils/MavenUtil.kt | 11 +- .../maven/wizards/MavenProjectAsyncBuilder.kt | 6 +- .../src/main/resources/META-INF/plugin.xml | 46 ++- .../maven.pom.model.version.hints/preview.xml | 9 + .../Maven4RedundantParentCoordinates.html | 5 + .../MavenBomPackagingInOldSchema.html | 5 + .../MavenModulesInMaven4.html | 5 + .../MavenNewModelVersionInOldMaven.html | 5 + .../MavenNoRootDefined.html | 5 + .../MavenRedundantVersion.html | 5 + .../metaInformation.json | 12 + .../messages/MavenDomBundle.properties | 29 +- .../messages/MavenSyncBundle.properties | 2 +- .../idea/maven/dom/MavenDomUtilTest.kt | 99 ++++++ .../MavenModelDocumentationProviderTest.kt | 29 ++ .../maven/dom/MavenModelValidationTest.kt | 25 -- .../maven/dom/MavenPackagingCompletionTest.kt | 2 +- ...avenPropertyCompletionAndResolutionTest.kt | 159 ++++++++++ .../editor/MavenModelSynchronizerTest.kt | 163 ++++++++++ ...edundantParentCoordinatesInspectionTest.kt | 143 +++++++++ .../dom/MavenBomInOldModelInspectionTest.kt | 80 +++++ .../dom/MavenModulesInMaven4InspectionTest.kt | 76 +++++ ...ewModelVersionInOldSchemaInspectionTest.kt | 176 +++++++++++ ...wModelVersionWithOldMavenInspectionTest.kt | 48 +++ .../dom/MavenNoRootDefinedInspectionTest.kt | 139 +++++++++ .../dom/MavenRedundantGroupIdTest.kt | 35 ++- .../dom/MavenRedundantVersionTest.kt | 97 ++++++ .../maven/quickfixes/UpdateXmlTo410Test.kt | 6 +- .../maven/testFramework/MavenDomTestCase.kt | 1 + .../MavenMultiVersionImportingTestCase.kt | 3 +- .../maven/testFramework/MavenTestCase.kt | 7 + 58 files changed, 2661 insertions(+), 234 deletions(-) delete mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.java create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inlay/MavenModelVersionInlayHintProvider.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/AbstractMavenRedundantParentInspection.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/Maven4RedundantParentCoordinatesInspection.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenBomPackagingInOldSchema.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenModulesInMaven4Inspection.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNewModelVersionInOldMavenInspection.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNoRootDefinedInspection.kt delete mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.java create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantVersionInspection.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSynchronizerService.kt create mode 100644 plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSyncronizerImpl.kt create mode 100644 plugins/maven/src/main/resources/inlayProviders/maven.pom.model.version.hints/preview.xml create mode 100644 plugins/maven/src/main/resources/inspectionDescriptions/Maven4RedundantParentCoordinates.html create mode 100644 plugins/maven/src/main/resources/inspectionDescriptions/MavenBomPackagingInOldSchema.html create mode 100644 plugins/maven/src/main/resources/inspectionDescriptions/MavenModulesInMaven4.html create mode 100644 plugins/maven/src/main/resources/inspectionDescriptions/MavenNewModelVersionInOldMaven.html create mode 100644 plugins/maven/src/main/resources/inspectionDescriptions/MavenNoRootDefined.html create mode 100644 plugins/maven/src/main/resources/inspectionDescriptions/MavenRedundantVersion.html create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenDomUtilTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProviderTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/editor/MavenModelSynchronizerTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/Maven4RedundantParentCoordinatesInspectionTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenBomInOldModelInspectionTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenModulesInMaven4InspectionTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionInOldSchemaInspectionTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionWithOldMavenInspectionTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNoRootDefinedInspectionTest.kt create mode 100644 plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantVersionTest.kt diff --git a/plugins/maven-server-api/src/main/java/org/jetbrains/idea/maven/model/MavenConstants.java b/plugins/maven-server-api/src/main/java/org/jetbrains/idea/maven/model/MavenConstants.java index c521d010766f..9a6a6c3351ee 100644 --- a/plugins/maven-server-api/src/main/java/org/jetbrains/idea/maven/model/MavenConstants.java +++ b/plugins/maven-server-api/src/main/java/org/jetbrains/idea/maven/model/MavenConstants.java @@ -5,10 +5,20 @@ import java.util.Arrays; import java.util.List; public final class MavenConstants { - public static final String MAVEN_4_XLMNS = "http://maven.apache.org/POM/4.1.0"; - public static final String MAVEN_4_XLMNS_HTTPS = "https://maven.apache.org/POM/4.1.0"; - public static final String MAVEN_4_XSD = "https://maven.apache.org/xsd/maven-4.1.0.xsd"; - public static final String MAVEN_4_XSD_HTTPS = "http://maven.apache.org/xsd/maven-4.1.0.xsd"; + public static final String MAVEN_4_XMLNS = "http://maven.apache.org/POM/4.1.0"; + public static final String MAVEN_4_XMLNS_HTTPS = "https://maven.apache.org/POM/4.1.0"; + public static final String MAVEN_4_XSD = "http://maven.apache.org/xsd/maven-4.1.0.xsd"; + public static final String MAVEN_4_XSD_HTTPS = "https://maven.apache.org/xsd/maven-4.1.0.xsd"; + public static final String MAVEN_4_XSD_UNDERSCORE = "http://maven.apache.org/maven-v4_1_0.xsd"; + public static final String MAVEN_4_XSD_HTTPS_UNDERSCORE = "https://maven.apache.org/maven-v4_1_0.xsd"; + + + public static final String MAVEN_3_XMLNS = "http://maven.apache.org/POM/4.0.0"; + public static final String MAVEN_3_XMLNS_HTTPS = "https://maven.apache.org/POM/4.0.0"; + public static final String MAVEN_3_XSD = "http://maven.apache.org/xsd/maven-4.0.0.xsd"; + public static final String MAVEN_3_XSD_HTTPS = "https://maven.apache.org/xsd/maven-4..0.xsd"; + + public static final String POM_EXTENSION = "pom"; public static final String POM_XML = "pom.xml"; diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/buildtool/quickfix/UpdateXmlsTo410.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/buildtool/quickfix/UpdateXmlsTo410.kt index 12f455338e16..211e424509d3 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/buildtool/quickfix/UpdateXmlsTo410.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/buildtool/quickfix/UpdateXmlsTo410.kt @@ -8,8 +8,12 @@ import com.intellij.openapi.project.Project import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.xml.XmlTag import org.jetbrains.idea.maven.model.MavenConstants -import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XLMNS +import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_3_XMLNS +import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_3_XMLNS_HTTPS +import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XMLNS +import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XMLNS_HTTPS import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XSD +import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XSD_HTTPS import org.jetbrains.idea.maven.project.MavenProjectBundle class UpdateXmlsTo410 : LocalQuickFix { @@ -24,16 +28,7 @@ class UpdateXmlsTo410 : LocalQuickFix { override fun applyFix(project: Project, descriptor: ProblemDescriptor) { val rootTag = findProjectTag(descriptor) ?: return - val xmlnsXsi = rootTag.getAttribute("xmlns:xsi") - rootTag.setAttribute("xmlns", MAVEN_4_XLMNS) - if (xmlnsXsi == null) { - rootTag.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") - } - rootTag.setAttribute("xsi:schemaLocation", "$MAVEN_4_XLMNS $MAVEN_4_XSD") - val modelVersion = rootTag.findSubTags("modelVersion") - if (modelVersion.isNotEmpty()) { - modelVersion.forEach { it.value.setText(MavenConstants.MODEL_VERSION_4_1_0) } - } + updateTagTo410(rootTag) } private fun findProjectTag(descriptor: ProblemDescriptor): XmlTag? { @@ -45,4 +40,32 @@ class UpdateXmlsTo410 : LocalQuickFix { return null } + companion object { + fun updateTagTo410(rootTag: XmlTag) { + val xmlnsXsi = rootTag.getAttribute("xmlns:xsi") + val xmlns = rootTag.getAttribute("xmlns")?.value + if(xmlns.isNullOrBlank()|| xmlns == MAVEN_3_XMLNS) { + rootTag.setAttribute("xmlns", MAVEN_4_XMLNS) + } else if(xmlns == MAVEN_3_XMLNS_HTTPS) { + rootTag.setAttribute("xmlns", MAVEN_4_XMLNS_HTTPS) + } + + if (xmlnsXsi == null) { + rootTag.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + } + + val schemaLocation = rootTag.getAttribute("xsi:schemaLocation")?.value + + if(schemaLocation.isNullOrBlank() || schemaLocation.startsWith(MAVEN_3_XMLNS)) { + rootTag.setAttribute("xsi:schemaLocation", "$MAVEN_4_XMLNS $MAVEN_4_XSD") + } else if(schemaLocation.startsWith(MAVEN_3_XMLNS_HTTPS)) { + rootTag.setAttribute("xsi:schemaLocation", "$MAVEN_4_XMLNS_HTTPS $MAVEN_4_XSD_HTTPS") + } + + val modelVersion = rootTag.findSubTags("modelVersion") + if (modelVersion.isNotEmpty()) { + modelVersion.forEach { it.value.setText(MavenConstants.MODEL_VERSION_4_1_0) } + } + } + } } diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenDomUtil.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenDomUtil.kt index 97817feb1b96..f4c092452065 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenDomUtil.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenDomUtil.kt @@ -28,6 +28,8 @@ import com.intellij.util.containers.ContainerUtil import com.intellij.util.xml.* import org.jetbrains.idea.maven.dom.model.* import org.jetbrains.idea.maven.model.MavenConstants +import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_0_0 +import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_1_0 import org.jetbrains.idea.maven.model.MavenCoordinate import org.jetbrains.idea.maven.model.MavenId import org.jetbrains.idea.maven.model.MavenResource @@ -37,6 +39,7 @@ import org.jetbrains.idea.maven.project.MavenProjectsManager import org.jetbrains.idea.maven.server.MavenDistribution import org.jetbrains.idea.maven.server.MavenDistributionsCache import org.jetbrains.idea.maven.utils.MavenLog +import org.jetbrains.idea.maven.utils.MavenUtil import org.jetbrains.idea.maven.utils.MavenUtil.isPomFileName import java.util.regex.Pattern @@ -71,6 +74,29 @@ object MavenDomUtil { return isPomFileName(file.getName()) } + @JvmStatic + fun isProjectFileWithModel410(file: PsiFile?): Boolean { + if (file !is XmlFile) return false + + val rootTag = file.getRootTag() + if (rootTag == null || "project" != rootTag.getName()) return false + + val xmlns = rootTag.getAttributeValue("xmlns") + if (xmlns != "http://maven.apache.org/POM/4.1.0" && xmlns != "https://maven.apache.org/POM/4.1.0"){ + return false + } + + if (!isPomFileName(file.getName())) return false + + val modelTag = rootTag.findSubTags("modelVersion").singleOrNull() + if (modelTag?.value?.text == MODEL_VERSION_4_1_0) return true + if (modelTag?.value?.text == MODEL_VERSION_4_0_0) return false + return MavenUtil.isMaven410( + rootTag?.getAttribute("xmlns")?.value, + rootTag?.getAttribute("xsi:schemaLocation")?.value) + + } + @JvmStatic fun getXmlProjectModelVersion(file: PsiFile?): @NlsSafe String? { if (file !is XmlFile) return null diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProvider.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProvider.java index 113cac69f974..d8c8cb060bde 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProvider.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProvider.java @@ -18,6 +18,7 @@ import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.maven.dom.references.MavenPsiElementWrapper; +import org.jetbrains.idea.maven.server.MavenDistributionsCache; import org.jetbrains.idea.maven.utils.MavenLog; import java.util.ArrayList; @@ -54,7 +55,8 @@ public final class MavenModelDocumentationProvider implements DocumentationProvi @Override public @Nls String getElementDescription(@NotNull PsiElement element, @NotNull ElementDescriptionLocation location) { return ReadAction.compute(() -> - getMavenElementDescription(element, location instanceof UsageViewTypeLocation ? DescKind.TYPE : DescKind.NAME, false) + getMavenElementDescription(element, + location instanceof UsageViewTypeLocation ? DescKind.TYPE : DescKind.NAME, false) ); } @@ -81,13 +83,26 @@ public final class MavenModelDocumentationProvider implements DocumentationProvi if (e instanceof XmlTag) { valueSuffix = ": " + bold[0] + ((XmlTag)e).getValue().getTrimmedText() + bold[1]; } - return type + br + name + valueSuffix; + var tip = getTip(name, kind, valueSuffix, e); + if (tip == null) { + return type + br + name + valueSuffix; + } + return type + br + name + valueSuffix + br + tip; } MavenLog.LOG.error("unexpected desc kind: " + kind); return null; } + private static @Nullable @Nls String getTip(String name, DescKind kind, String suffix, PsiElement e) { + if (!"project.modelVersion".equals(name)) return null; + var project = e.getProject(); + if (project.isDefault()) return null; + var mavenVersion = MavenDistributionsCache.getInstance(project) + .getMavenDistribution(e.getContainingFile().getVirtualFile()); + return MavenDomBundle.message("maven.version.tip", mavenVersion.getVersion()); + } + private static @NlsContexts.DetailedDescription String buildPropertyName(PsiElement e, boolean property) { if (property) return DescriptiveNameUtil.getDescriptiveName(e); //NON-NLS - suprress warning diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenModelVersionConverter.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenModelVersionConverter.kt index 0905a924ebc1..9f0d3427bfa6 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenModelVersionConverter.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenModelVersionConverter.kt @@ -21,9 +21,10 @@ import org.jetbrains.idea.maven.dom.MavenDomBundle import org.jetbrains.idea.maven.dom.MavenDomUtil.isAtLeastMaven4 import org.jetbrains.idea.maven.model.MavenConstants import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_1_0 +import org.jetbrains.idea.maven.server.MavenDistributionsCache import org.jetbrains.idea.maven.utils.MavenUtil -class MavenModelVersionConverter : MavenConstantListConverter() { +class MavenModelVersionConverter : MavenConstantListConverter(false) { override fun getValues(context: ConvertContext): Collection { return if (isAtLeastMaven4(context.getFile().getVirtualFile(), context.getProject())) { VALUES_MAVEN_4 @@ -43,7 +44,9 @@ class MavenModelVersionConverter : MavenConstantListConverter() { } override fun getErrorMessage(s: String?, context: ConvertContext): String? { - return MavenDomBundle.message("inspection.message.unsupported.model.version.only.version.supported", getValues(context)) + val project = context.project + val version = MavenDistributionsCache.getInstance(project).getMavenDistribution(context.file.virtualFile).version + return MavenDomBundle.message("inspection.message.unsupported.model.version.only.version.supported", getValues(context), version) } companion object { diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.java deleted file mode 100644 index c3277d6071bc..000000000000 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.jetbrains.idea.maven.dom.converters; - -import com.intellij.util.xml.ConvertContext; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.idea.maven.project.MavenProject; - -import java.util.Collection; - -public class MavenPackagingConverter extends MavenProjectConstantListConverter { - public MavenPackagingConverter() { - super(false); - } - - @Override - protected Collection getValues(@NotNull ConvertContext context, @NotNull MavenProject project) { - return project.getSupportedPackagings(); - } -} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.kt new file mode 100644 index 000000000000..04b43b05f0cb --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/converters/MavenPackagingConverter.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.idea.maven.dom.converters + +import com.intellij.util.xml.ConvertContext +import com.intellij.util.xml.DomManager +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.importing.MavenImporter +import org.jetbrains.idea.maven.model.MavenConstants +import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_0_0 +import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_1_0 +import org.jetbrains.idea.maven.project.MavenProject + +class MavenPackagingConverter : MavenProjectConstantListConverter(false) { + override fun getValues(context: ConvertContext, project: MavenProject): Collection { + val result = mutableSetOf() + result.addAll(DEFAULT_PACKAGES) + for (each: MavenImporter in MavenImporter.getSuitableImporters(project)) { + each.getSupportedPackagings(result) + } + val file = context.file + val model = + DomManager.getDomManager(file.getProject()).getFileElement(file, MavenDomProjectModel::class.java) + val rootElement = model?.rootElement + val modelVersion = rootElement?.modelVersion?.stringValue?.trim() + if (modelVersion != MODEL_VERSION_4_0_0) { + result.addAll(MAVEN_4_SPECIFIC) + } + return result + } + + companion object { + val DEFAULT_PACKAGES: Set = setOf(MavenConstants.TYPE_POM, MavenConstants.TYPE_JAR, "ejb", "ejb-client", "war", "ear", "bundle", "maven-plugin") + val MAVEN_4_SPECIFIC: Set = setOf("bom") + } +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inlay/MavenModelVersionInlayHintProvider.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inlay/MavenModelVersionInlayHintProvider.kt new file mode 100644 index 000000000000..4f849cb86ffd --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inlay/MavenModelVersionInlayHintProvider.kt @@ -0,0 +1,40 @@ +// 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.maven.dom.inlay + +import com.intellij.codeInsight.hints.declarative.* +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlFile +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.MavenDomUtil +import org.jetbrains.idea.maven.model.MavenConstants +import org.jetbrains.idea.maven.project.MavenProjectsManager + +class MavenModelVersionInlayHintProvider : InlayHintsProvider { + override fun createCollector(file: PsiFile, editor: Editor): MavenModelVersionInlayHintCollector = MavenModelVersionInlayHintCollector(editor) + + override fun isDumbAware(): Boolean = true +} + +class MavenModelVersionInlayHintCollector(val editor: Editor) : OwnBypassCollector { + override fun collectHintsForFile(file: PsiFile, sink: InlayTreeSink) { + if (file !is XmlFile) return + val manager = MavenProjectsManager.getInstance(file.project) + val vFile = file.virtualFile + if (manager.findProject(vFile) == null) return + val modelTag = file.rootTag?.findFirstSubTag("modelVersion") ?: return + val line = editor.document.getLineNumber(modelTag.textOffset) + val modelVersion = MavenDomUtil.getXmlProjectModelVersion(file) ?: return + if (modelVersion == MavenConstants.MODEL_VERSION_4_0_0) { + sink.addPresentation(EndOfLinePosition(line), hintFormat = HintFormat.default) { + text(MavenDomBundle.message("maven.version.inlay.hint.text.3")) + } + } + else if (modelVersion == MavenConstants.MODEL_VERSION_4_1_0) { + sink.addPresentation(EndOfLinePosition(line), hintFormat = HintFormat.default) { + text(MavenDomBundle.message("maven.version.inlay.hint.text.4")) + } + } + } + +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/AbstractMavenRedundantParentInspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/AbstractMavenRedundantParentInspection.kt new file mode 100644 index 000000000000..ba0eccf927eb --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/AbstractMavenRedundantParentInspection.kt @@ -0,0 +1,75 @@ +// 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.maven.dom.inspections + +import com.intellij.codeInspection.* +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import com.intellij.util.xml.DomManager +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.server.MavenDistribution + +abstract class AbstractMavenRedundantParentInspection : XmlSuppressableInspectionTool() { + override fun getGroupDisplayName(): String { + return MavenDomBundle.message("inspection.group") + } + + protected abstract val elementName: String + abstract override fun getShortName(): String + + override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { + if (file is XmlFile && file.isPhysical()) { + val model = + DomManager.getDomManager(file.getProject()).getFileElement(file, MavenDomProjectModel::class.java) + + + if (model != null) { + val projectModel = model.getRootElement() + if (projectModel == null) return null + + if (!supportedForFile(file)) { + return null + } + + val selfValue = getSelfValue(projectModel) + if (selfValue != null && !selfValue.isEmpty()) { + val parentValue = getParentValue(projectModel, file.project) + if (selfValue == parentValue) { + val xmlTag = getXmlTag(projectModel) ?: return null + + val fix: LocalQuickFix = object : LocalQuickFix { + + + override fun getFamilyName(): @IntentionFamilyName String { + return MavenDomBundle.message("inspection.redundant.element.fix", elementName) + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + descriptor.getPsiElement().delete() + } + } + + return arrayOf(manager.createProblemDescriptor(xmlTag, + MavenDomBundle.message("inspection.redundant.element.fix.description", elementName), + fix, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOnTheFly) + ) + } + } + } + } + + return null + } + + abstract fun getXmlTag(projectModel: MavenDomProjectModel): XmlTag? + + abstract fun getSelfValue(projectModel: MavenDomProjectModel): String? + abstract fun getParentValue(projectModel: MavenDomProjectModel, project: Project): String? + + protected abstract fun supportedForFile(file: XmlFile): Boolean + +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/Maven4RedundantParentCoordinatesInspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/Maven4RedundantParentCoordinatesInspection.kt new file mode 100644 index 000000000000..c7ef5dce0c13 --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/Maven4RedundantParentCoordinatesInspection.kt @@ -0,0 +1,88 @@ +// 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.maven.dom.inspections + +import com.intellij.codeInspection.* +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlFile +import com.intellij.util.xml.DomManager +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.model.MavenDomParent +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.server.MavenDistributionsCache +import org.jetbrains.idea.maven.server.isMaven4 + +class Maven4RedundantParentCoordinatesInspection : XmlSuppressableInspectionTool() { + + override fun getShortName(): String = "Maven4RedundantParentCoordinates" + + @Suppress("NULLABILITY_MISMATCH_BASED_ON_EXPLICIT_TYPE_ARGUMENTS_FOR_JAVA") + override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { + val project = file.project + + if (file is XmlFile && file.isPhysical()) { + val dist = MavenDistributionsCache.getInstance(file.project).getMavenDistribution(file.virtualFile) + if (!dist.isMaven4()) return null + val dom = + DomManager.getDomManager(file.getProject()).getFileElement(file, MavenDomProjectModel::class.java) + ?: return null + + val model = dom.rootElement + val declaredParentGroupId = model.mavenParent.groupId.stringValue + val declaredParentVersion = model.mavenParent.version.stringValue + val declaredParentArtifactId = model.mavenParent.artifactId.stringValue + if (declaredParentGroupId.isNullOrEmpty() && declaredParentVersion.isNullOrEmpty()) return null + val parentFile = model.mavenParent.relativePath.value ?: file.parent?.parent?.findFile("pom.xml") ?: return null + if (parentFile is XmlFile && parentFile.isPhysical()) { + val parentDom = + DomManager.getDomManager(project).getFileElement(parentFile, MavenDomProjectModel::class.java) + ?: return null + val parentModel = parentDom.rootElement + val parentGroupId = parentModel.groupId.stringValue + val parentVersion = parentModel.version.stringValue + val parentArtifactId = parentModel.artifactId.stringValue + + if (declaredParentGroupId != parentGroupId) return null + if (declaredParentVersion != parentVersion) return null + if (declaredParentArtifactId != parentArtifactId) return null + val fix = getCleanParentFix(model.mavenParent) + return listOf(model.mavenParent.groupId, + model.mavenParent.artifactId, + model.mavenParent.version) + .filter { it.exists() } + .mapNotNull { it.getXmlTag() } + .map { + manager.createProblemDescriptor(it, + MavenDomBundle.message("inspection.redundant.parent.coordinates.maven.4"), + fix, + ProblemHighlightType.WARNING, + isOnTheFly) + }.toTypedArray() + + + } + } + return null + } + + private fun getCleanParentFix(mavenParent: MavenDomParent): LocalQuickFix { + return object : LocalQuickFix { + override fun getFamilyName(): @IntentionFamilyName String { + return MavenDomBundle.message("inspection.redundant.parent.coordinates.maven.4") + } + + override fun getName(): @IntentionName String { + return MavenDomBundle.message("inspection.redundant.parent.coordinates.maven.4.clean") + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + mavenParent.groupId.xmlTag?.delete() + mavenParent.artifactId.xmlTag?.delete() + mavenParent.version.xmlTag?.delete() + mavenParent.xmlTag?.collapseIfEmpty() + } + } + } +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenBomPackagingInOldSchema.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenBomPackagingInOldSchema.kt new file mode 100644 index 000000000000..c047eec276cc --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenBomPackagingInOldSchema.kt @@ -0,0 +1,45 @@ +// 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.maven.dom.inspections + +import com.intellij.codeHighlighting.HighlightDisplayLevel +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.util.xml.DomFileElement +import com.intellij.util.xml.highlighting.BasicDomElementsInspection +import com.intellij.util.xml.highlighting.DomElementAnnotationHolder +import org.jetbrains.idea.maven.buildtool.quickfix.UpdateXmlsTo410 +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.converters.MavenPackagingConverter.Companion.MAVEN_4_SPECIFIC +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_1_0 + +class MavenBomPackagingInOldSchema : BasicDomElementsInspection(MavenDomProjectModel::class.java) { + override fun getGroupDisplayName(): String { + return MavenDomBundle.message("inspection.group") + } + + override fun getDefaultLevel(): HighlightDisplayLevel { + return HighlightDisplayLevel.ERROR + } + + override fun checkFileElement( + domFileElement: DomFileElement, + holder: DomElementAnnotationHolder, + ) { + val projectModel = domFileElement.getRootElement() + + if (!projectModel.modelVersion.exists() || projectModel.modelVersion.stringValue == MODEL_VERSION_4_1_0) return + val packaging = if (projectModel.packaging.exists()) projectModel.packaging.stringValue else return + if (packaging == null || projectModel.packaging.stringValue !in MAVEN_4_SPECIFIC) return + + holder.createProblem(projectModel.modelVersion, + HighlightSeverity.ERROR, + MavenDomBundle.message("inspection.new.packaging.in.old.model", packaging), + UpdateXmlsTo410() + ) + holder.createProblem(projectModel.packaging, + HighlightSeverity.ERROR, + MavenDomBundle.message("inspection.new.packaging.in.old.model", packaging), + UpdateXmlsTo410() + ) + } +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenModulesInMaven4Inspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenModulesInMaven4Inspection.kt new file mode 100644 index 000000000000..6f82a34f0123 --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenModulesInMaven4Inspection.kt @@ -0,0 +1,80 @@ +// 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.maven.dom.inspections + +import com.intellij.codeInspection.* + +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiFile +import com.intellij.psi.XmlElementFactory +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import com.intellij.util.containers.addAllIfNotNull +import com.intellij.util.containers.addIfNotNull +import com.intellij.util.xml.DomManager +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.MavenDomUtil +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel + +class MavenModulesInMaven4Inspection : XmlSuppressableInspectionTool() { + + override fun getShortName(): String = "MavenModulesInMaven4" + + override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { + if (file is XmlFile && file.isPhysical()) { + + if (!MavenDomUtil.isProjectFileWithModel410(file)) return null + val dom = + DomManager.getDomManager(file.getProject()).getFileElement(file, MavenDomProjectModel::class.java) + ?: return null + + val model = dom.rootElement + if (!model.modules.exists()) return null + val tag = model.modules.xmlTag?:return null + val fix = replaceModulesToSubprojects(model) + + + + return arrayOf( + manager.createProblemDescriptor( + tag, + MavenDomBundle.message("inspection.modules.tag.in.maven.4"), + fix, + ProblemHighlightType.WARNING, + isOnTheFly + ) + ) + } + return null + } + + private fun replaceModulesToSubprojects(model: MavenDomProjectModel): LocalQuickFix { + return object: LocalQuickFix { + override fun getFamilyName(): @IntentionFamilyName String { + return MavenDomBundle.message("inspection.modules.tag.in.maven.4") + } + + override fun getName(): @IntentionName String { + return MavenDomBundle.message("inspection.modules.tag.in.maven.4.name") + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val modulesList = model.modules.modules.map { + it.stringValue + } + + val factory = XmlElementFactory.getInstance(project) + val subprojectsTag = factory.createTagFromText("") + modulesList.forEach { + val tag = factory.createTagFromText("$it") + subprojectsTag.addSubTag(tag, false) + + } + model.modules.xmlTag?.replace(subprojectsTag) + } + + } + } +} diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNewModelVersionInOldMavenInspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNewModelVersionInOldMavenInspection.kt new file mode 100644 index 000000000000..a0b9fddfc34b --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNewModelVersionInOldMavenInspection.kt @@ -0,0 +1,174 @@ +// 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.maven.dom.inspections + +import com.intellij.codeHighlighting.HighlightDisplayLevel +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessListener +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.project.Project +import com.intellij.platform.eel.provider.utils.EelPathUtils +import com.intellij.util.xml.DomFileElement +import com.intellij.util.xml.highlighting.BasicDomElementsInspection +import com.intellij.util.xml.highlighting.DomElementAnnotationHolder +import org.jetbrains.idea.maven.buildtool.MavenSyncSpec +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.execution.MavenRunConfigurationType +import org.jetbrains.idea.maven.execution.MavenRunnerParameters +import org.jetbrains.idea.maven.execution.MavenRunnerSettings +import org.jetbrains.idea.maven.execution.SyncBundle +import org.jetbrains.idea.maven.model.MavenConstants +import org.jetbrains.idea.maven.project.MavenProject +import org.jetbrains.idea.maven.project.MavenProjectsManager +import org.jetbrains.idea.maven.project.MavenWorkspaceSettingsComponent +import org.jetbrains.idea.maven.project.MavenWrapper +import org.jetbrains.idea.maven.server.MavenDistributionsCache +import org.jetbrains.idea.maven.server.MavenServerManager +import org.jetbrains.idea.maven.server.isMaven4 +import java.nio.file.FileAlreadyExistsException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import kotlin.io.path.* + + +private val VERSION_TO_UPDATE_TO = "4.0.0-rc-4" + +class MavenNewModelVersionInOldMavenInspection : BasicDomElementsInspection(MavenDomProjectModel::class.java) { + override fun getGroupDisplayName(): String { + return MavenDomBundle.message("inspection.group") + } + + override fun getDefaultLevel(): HighlightDisplayLevel { + return HighlightDisplayLevel.ERROR + } + + override fun checkFileElement( + domFileElement: DomFileElement, + holder: DomElementAnnotationHolder, + ) { + + val project = domFileElement.file.project + val psiFile = domFileElement.file + val vFile = psiFile.virtualFile + val mavenProject = + MavenProjectsManager.getInstance(project).findProject(vFile) ?: return + + val rootProject = MavenProjectsManager.getInstance(project).findRootProject(mavenProject) ?: return + + val projectModel = domFileElement.getRootElement() + if (projectModel.modelVersion.stringValue == MavenConstants.MODEL_VERSION_4_0_0) return + + val distribution = MavenDistributionsCache.getInstance(psiFile.project).getMavenDistribution(psiFile.virtualFile) + if (distribution.isMaven4()) return + + holder.createProblem(projectModel.modelVersion, + HighlightSeverity.ERROR, + MavenDomBundle.message("inspection.new.model.version.with.old.maven"), + UpdateMavenWrapper(rootProject, VERSION_TO_UPDATE_TO) + + ) + } +} + +class UpdateMavenWrapper(@Suppress("ActionIsNotPreviewFriendly") val mavenProject: MavenProject, val version: String) : LocalQuickFix { + override fun getName(): String { + return SyncBundle.message("maven.sync.quickfixes.update.maven.version", version) + } + + override fun getFamilyName(): @IntentionFamilyName String { + return name + } + + private fun createTempProject(project: Project): Path { + val tmp = EelPathUtils.createTemporaryDirectory(project, + prefix = "mvn-wrapper-update", + deleteOnExit = true) + tmp.resolve("pom.xml").writeText(createDummyPomContent()) + return tmp + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + + val workingDir = mavenProject.directoryFile + val tempDir = createTempProject(project) + MavenRunConfigurationType.runConfiguration(project, + MavenRunnerParameters( + true, + tempDir.toString(), + null as String?, + listOf("wrapper:wrapper"), + null + ).also { it.cmdOptions = "-N" }, + null, + MavenRunnerSettings().also { + it.setVmOptions("-Dmaven=$version") + } + ) { + it.processHandler?.addProcessListener(object : ProcessListener { + override fun processTerminated(event: ProcessEvent) { + if (event.exitCode == 0) { + copyFromTempDir(tempDir, workingDir.toNioPath()) + workingDir.refresh(true, true) { + MavenWorkspaceSettingsComponent.getInstance(project).settings.generalSettings.mavenHomeType = MavenWrapper + MavenDistributionsCache.getInstance(project).cleanCaches() + MavenServerManager.getInstance().shutdownMavenConnectors(project){true} + MavenProjectsManager.getInstance(project).forceUpdateAllProjectsOrFindAllAvailablePomFiles() + } + + } + removeTempDirSafely(tempDir) + } + }) + } + } + + private fun copyFromTempDir(from: Path, to: Path) { + Files.walk(from).use { stream -> + stream.forEach { file -> + val relative = from.relativize(file) + if (!relative.toString().isEmpty() + && !file.name.equals("pom.xml", true)) { + val newFile = to.resolve(relative) + if (file.isDirectory()) { + try { + Files.createDirectory(newFile) + } + catch (_: FileAlreadyExistsException) { + } + } + else { + Files.copy(file, newFile, StandardCopyOption.REPLACE_EXISTING) + } + } + } + } + } + + @OptIn(ExperimentalPathApi::class) + private fun removeTempDirSafely(tempDir: Path) { + try { + tempDir.deleteRecursively() + } + catch (_: Throwable) { + } + + } + + private fun createDummyPomContent(): String { + return """ + + +4.0.0 +${mavenProject.mavenId.groupId} +${mavenProject.mavenId.artifactId} +${mavenProject.mavenId.version} + + """ + } +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNoRootDefinedInspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNoRootDefinedInspection.kt new file mode 100644 index 000000000000..0bc3948efa2f --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenNoRootDefinedInspection.kt @@ -0,0 +1,97 @@ +// 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.maven.dom.inspections + +import com.intellij.codeHighlighting.HighlightDisplayLevel +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.util.IntentionFamilyName +import com.intellij.codeInspection.util.IntentionName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import com.intellij.util.xml.DomManager +import com.intellij.util.xml.highlighting.BasicDomElementsInspection +import org.jetbrains.idea.maven.dom.MavenDomBundle +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.model.MavenConstants.MODEL_VERSION_4_1_0 +import org.jetbrains.idea.maven.project.MavenProjectsManager +import kotlin.io.path.isDirectory + +class MavenNoRootDefinedInspection : BasicDomElementsInspection(MavenDomProjectModel::class.java) { + override fun getGroupDisplayName(): String { + return MavenDomBundle.message("inspection.group") + } + + override fun getDefaultLevel(): HighlightDisplayLevel { + return HighlightDisplayLevel.WARNING + } + + override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { + if (file is XmlFile && file.isPhysical()) { + val projectManager = MavenProjectsManager.getInstanceIfCreated(file.project) ?: return null + val rootMavenProject = projectManager.rootProjects.singleOrNull { file.virtualFile.equals(it.file) } ?: return null + val model = + DomManager.getDomManager(file.getProject()).getFileElement(file, MavenDomProjectModel::class.java) + ?: return null + + @Suppress("USELESS_ELVIS") + //KTLC-284 + + val rootElement = model.rootElement ?: return null + val rootTag = model.rootTag ?: return null + + if (rootMavenProject.file.parent.toNioPath().resolve(".mvn").isDirectory()) return null; + if (rootElement.modelVersion.stringValue == MODEL_VERSION_4_1_0 + && model.rootTag?.getAttributeValue("root")?.toBoolean() == true) { + return null + } + + + if (rootElement.modelVersion.stringValue == MODEL_VERSION_4_1_0) { + return arrayOf(manager.createProblemDescriptor(rootTag, + MavenDomBundle.message("inspection.absence.root.dir.description"), + arrayOf(fixAddRootTag, fixAddMvnDirectoryInRootDir), ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOnTheFly, false)) + } + return arrayOf(manager.createProblemDescriptor(rootTag, + MavenDomBundle.message("inspection.absence.root.dir.description"), + fixAddMvnDirectoryInRootDir, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + isOnTheFly)) + } + + return null + } + + + private val fixAddMvnDirectoryInRootDir: LocalQuickFix = object : LocalQuickFix { + override fun getName(): @IntentionName String { + return MavenDomBundle.message("inspection.absence.dir.fix.create.dir") + } + + override fun getFamilyName(): @IntentionFamilyName String { + return MavenDomBundle.message("inspection.absence.root.dir.description") + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + descriptor.psiElement.containingFile.containingDirectory.createSubdirectory(".mvn") + } + } + + private val fixAddRootTag: LocalQuickFix = object : LocalQuickFix { + override fun getName(): @IntentionName String { + return MavenDomBundle.message("inspection.absence.dir.fix.add.root") + } + + override fun getFamilyName(): @IntentionFamilyName String { + return MavenDomBundle.message("inspection.absence.root.dir.description") + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + (descriptor.psiElement as? XmlTag)?.setAttribute("root", "true") + } + } + +} \ No newline at end of file diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.java deleted file mode 100644 index ce2ea36814fa..000000000000 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.java +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package org.jetbrains.idea.maven.dom.inspections; - -import com.intellij.codeInspection.*; -import com.intellij.codeInspection.util.IntentionFamilyName; -import com.intellij.openapi.project.DumbAware; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiFile; -import com.intellij.psi.xml.XmlFile; -import com.intellij.psi.xml.XmlTag; -import com.intellij.util.xml.DomFileElement; -import com.intellij.util.xml.DomManager; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.idea.maven.dom.MavenDomBundle; -import org.jetbrains.idea.maven.dom.model.MavenDomParent; -import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel; - -public final class MavenRedundantGroupIdInspection extends XmlSuppressableInspectionTool implements DumbAware { - - @Override - public @NotNull String getGroupDisplayName() { - return MavenDomBundle.message("inspection.group"); - } - - @Override - public @NotNull String getShortName() { - return "MavenRedundantGroupId"; - } - - @Override - public ProblemDescriptor @Nullable [] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) { - if (file instanceof XmlFile && file.isPhysical()) { - DomFileElement model = - DomManager.getDomManager(file.getProject()).getFileElement((XmlFile)file, MavenDomProjectModel.class); - - if (model != null) { - MavenDomProjectModel projectModel = model.getRootElement(); - - String groupId = projectModel.getGroupId().getStringValue(); - if (groupId != null && !groupId.isEmpty()) { - MavenDomParent parent = projectModel.getMavenParent(); - - String parentGroupId = parent.getGroupId().getStringValue(); - - if (groupId.equals(parentGroupId)) { - XmlTag xmlTag = projectModel.getGroupId().getXmlTag(); - - LocalQuickFix fix = new MavenRedundantGroupIdQuickFix(); - - return new ProblemDescriptor[]{ - manager.createProblemDescriptor(xmlTag, - MavenDomBundle.message("inspection.redundant.groupId.fix.description"), - fix, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, isOnTheFly) - }; - } - } - - } - } - - return null; - } - - private static final class MavenRedundantGroupIdQuickFix implements LocalQuickFix, DumbAware { - @Override - public @IntentionFamilyName @NotNull String getFamilyName() { - return MavenDomBundle.message("inspection.redundant.groupId.fix"); - } - - @Override - public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { - descriptor.getPsiElement().delete(); - } - } -} diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.kt new file mode 100644 index 000000000000..03f048600fd4 --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantGroupIdInspection.kt @@ -0,0 +1,22 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.maven.dom.inspections + +import com.intellij.openapi.project.Project +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.server.MavenDistribution + +class MavenRedundantGroupIdInspection : AbstractMavenRedundantParentInspection() { + override val elementName: String = "groupId" + + override fun getShortName(): String = "MavenRedundantGroupId" + + override fun getXmlTag(projectModel: MavenDomProjectModel): XmlTag? = projectModel.groupId.getXmlTag() + + override fun getSelfValue(projectModel: MavenDomProjectModel): String? = projectModel.groupId.stringValue + + override fun getParentValue(projectModel: MavenDomProjectModel, project: Project): String? = projectModel.mavenParent.groupId.stringValue + + override fun supportedForFile(file: XmlFile): Boolean= true +} diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantVersionInspection.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantVersionInspection.kt new file mode 100644 index 000000000000..64642fc21647 --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/inspections/MavenRedundantVersionInspection.kt @@ -0,0 +1,23 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.maven.dom.inspections + +import com.intellij.openapi.project.Project +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlTag +import org.jetbrains.idea.maven.dom.model.MavenDomProjectModel +import org.jetbrains.idea.maven.server.MavenDistribution + + +class MavenRedundantVersionInspection : AbstractMavenRedundantParentInspection() { + override val elementName: String = "version" + + override fun getShortName(): String = "MavenRedundantVersion" + + override fun getXmlTag(projectModel: MavenDomProjectModel): XmlTag? = projectModel.version.getXmlTag() + + override fun getSelfValue(projectModel: MavenDomProjectModel): String? = projectModel.version.stringValue + + override fun getParentValue(projectModel: MavenDomProjectModel, project: Project): String? = projectModel.mavenParent.version.stringValue + + override fun supportedForFile(file: XmlFile): Boolean = true +} diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/references/MavenPropertyPsiReference.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/references/MavenPropertyPsiReference.java index 1f0f9cc36155..1cfae21bc1d1 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/references/MavenPropertyPsiReference.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/dom/references/MavenPropertyPsiReference.java @@ -51,6 +51,7 @@ import org.jetbrains.idea.maven.model.MavenId; import org.jetbrains.idea.maven.model.MavenPlugin; import org.jetbrains.idea.maven.plugins.api.MavenPluginDescriptor; import org.jetbrains.idea.maven.project.MavenProject; +import org.jetbrains.idea.maven.project.MavenProjectsManager; import org.jetbrains.idea.maven.project.MavenSettingsCache; import org.jetbrains.idea.maven.utils.MavenUtil; import org.jetbrains.idea.maven.vfs.MavenPropertiesVirtualFileSystem; @@ -162,9 +163,16 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca return myElement; } + if (myText.equals(MULTIPROJECT_DIR_PROP)) { MavenProject rootProject = myProjectsManager.findRootProject(myMavenProject); - if(rootProject == null) return null; + if (rootProject == null) return null; + return getBaseDir(rootProject); + } + + if (isModel410() && (myText.equals("project.rootDirectory") || myText.equals("session.rootDirectory") || myText.equals("session.topDirectory"))) { + MavenProject rootProject = myProjectsManager.findRootProject(myMavenProject); + if (rootProject == null) return null; return getBaseDir(rootProject); } @@ -306,6 +314,11 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca return null; } + private boolean isModel410() { + var e = getElement(); + return MavenDomUtil.isProjectFileWithModel410(e.getContainingFile()); + } + private @Nullable PsiElement resolveToCustomSystemProperty(@NotNull String propertyName, @Nullable String propertyValue) { if (propertyValue == null) return null; @@ -319,6 +332,12 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca return PsiManager.getInstance(myProject).findDirectory(mavenProject.getDirectoryFile()); } + private PsiDirectory getRootDir(@NotNull MavenProject mavenProject) { + MavenProject root = MavenProjectsManager.getInstance(myProject).findRootProject(mavenProject); + if (root == null) return getBaseDir(mavenProject); + return PsiManager.getInstance(myProject).findDirectory(root.getDirectoryFile()); + } + private PsiElement resolveConfigFileProperty(@SystemIndependent String fileRelativePath, String propertyValue) { VirtualFile baseDir = VfsUtil.findFile(MavenUtil.getBaseDir(myMavenProject.getDirectoryFile()), false); if (baseDir != null) { @@ -360,7 +379,7 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca String pathWithProjectPrefix = "project." + path; if (!MavenModelClassesProperties.isPathValid(MavenModelClassesProperties.MAVEN_PROJECT_CLASS, path) - && !MavenModelClassesProperties.isPathValid(MavenModelClassesProperties.MAVEN_MODEL_CLASS, path)) { + && !MavenModelClassesProperties.isPathValid(MavenModelClassesProperties.MAVEN_MODEL_CLASS, path)) { if (!schemaHasProperty(getSchemaUrl(), pathWithProjectPrefix)) return null; } @@ -427,6 +446,14 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca PsiDirectory baseDir = getBaseDir(mavenProject); addVariant(result, "basedir", baseDir, prefix, RepositoryLibraryLogo); + + if (isModel410()) { + PsiDirectory rootDir = getRootDir(mavenProject); + addVariant(result, "rootDirectory", rootDir, "project.", RepositoryLibraryLogo); + addVariant(result, "rootDirectory", rootDir, "session.", RepositoryLibraryLogo); + addVariant(result, "topDirectory", rootDir, "session.", RepositoryLibraryLogo); + } + addVariant(result, "basedir", baseDir, prefix, RepositoryLibraryLogo); if (prefix == null) { result.add(createLookupElement(baseDir, "project.baseUri", RepositoryLibraryLogo)); result.add(createLookupElement(baseDir, "pom.baseUri", RepositoryLibraryLogo)); @@ -626,7 +653,7 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca @Override public @NotNull LocalQuickFix @Nullable [] getQuickFixes() { - return new LocalQuickFix[]{ new MyLocalQuickFix() }; + return new LocalQuickFix[]{new MyLocalQuickFix()}; } private static class MyLocalQuickFix implements LocalQuickFix { @@ -648,6 +675,7 @@ public class MavenPropertyPsiReference extends MavenPsiReference implements Loca /** * If "build-helper-maven-plugin" has `parse-version` goal, probably it could resolve properties starting with a defined prefix * to something related to the version from the `version` tag (e.g., `${parsedVersion.majorVersion}`) + * * @see mojohaus documentation */ private @Nullable PsiElement resolveAsParsedVersion(@NotNull String propertyText, @NotNull MavenProject mavenProject) { diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSynchronizerService.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSynchronizerService.kt new file mode 100644 index 000000000000..36d9d84cf4d8 --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSynchronizerService.kt @@ -0,0 +1,118 @@ +// 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.maven.editor + +import com.intellij.application.options.editor.WebEditorOptions +import com.intellij.ide.plugins.DynamicPluginListener +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.openapi.application.EDT +import com.intellij.openapi.command.CommandEvent +import com.intellij.openapi.command.CommandListener +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.registry.Registry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import org.jetbrains.idea.maven.project.MavenProjectsManager +import kotlin.streams.asSequence + +@Service(Service.Level.PROJECT) +class MavenModelVersionSynchronizerService(private val project: Project, val cs: CoroutineScope) { + + companion object { + @JvmField + internal val SYNCHRONIZER_KEY: Key = Key.create("maven.version.xml.sync") + + @JvmField + internal val SKIP_COMMAND_KEY: Key = Key.create("maven.version.synchronizer.skip.command") + } + + private val writeLock = Semaphore(1) + + + private fun recreateSynchronizersFor(editors: List) { + cs.launch { + writeLock.withPermit { + editors.forEach { editor -> + editor.getUserData(SYNCHRONIZER_KEY)?.let { + Disposer.dispose(it) + } + ensureSynchronizerCreated(editor) + } + } + } + } + + fun scheduleEnsureSynchronizerCreated(editor: EditorImpl) { + cs.launch { + writeLock.withPermit { + ensureSynchronizerCreated(editor) + } + } + } + + private suspend fun ensureSynchronizerCreated(editor: EditorImpl) { + if (!Registry.`is`("maven.sync.model.editing")) return + if (editor.getUserData(SYNCHRONIZER_KEY) != null) return + + val mavenProjectManager = MavenProjectsManager.getInstanceIfCreated(project) ?: return + val file = FileDocumentManager.getInstance().getFile(editor.document) ?: return + if (mavenProjectManager.findProject(file) == null) return + withContext(Dispatchers.EDT) { + if (editor.isDisposed || project.isDisposed()) return@withContext + MavenModelVersionSyncronizerImpl(editor, project).listenForDocumentChanges() + } + } + + + class MyCommandListener : CommandListener { + override fun beforeCommandFinished(event: CommandEvent) { + findSynchronizers(event.document).forEach { it.beforeCommandFinished() } + } + + private fun findSynchronizers(document: Document?): Sequence = + if (document == null || !Registry.`is`("maven.sync.model.editing")) + emptySequence() + else + EditorFactory.getInstance().editors(document, null) + .asSequence() + .mapNotNull { editor -> editor.getUserData(SYNCHRONIZER_KEY) } + } + + class MyDynamicPluginListener : DynamicPluginListener { + override fun pluginLoaded(pluginDescriptor: IdeaPluginDescriptor) { + recreateSynchronizers() + } + + override fun pluginUnloaded(pluginDescriptor: IdeaPluginDescriptor, isUpdate: Boolean) { + recreateSynchronizers() + } + + private fun recreateSynchronizers() { + EditorFactory.getInstance().getAllEditors().groupBy { it.project }.forEach { (project, editors) -> + project?.service()?.recreateSynchronizersFor(editors.filterIsInstance()) + } + } + } +} + +class MavenModelVersionEditorFactoryListener : EditorFactoryListener { + override fun editorCreated(event: EditorFactoryEvent) { + val editor = event.editor + val project = editor.project + project?.service()?.scheduleEnsureSynchronizerCreated(editor as? EditorImpl ?: return) + } +} + diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSyncronizerImpl.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSyncronizerImpl.kt new file mode 100644 index 000000000000..ce1e5dc9a84a --- /dev/null +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/editor/MavenModelVersionSyncronizerImpl.kt @@ -0,0 +1,284 @@ +package org.jetbrains.idea.maven.editor + +import com.intellij.codeInsight.lookup.LookupManager +import com.intellij.codeInsight.lookup.impl.LookupImpl +import com.intellij.lang.xml.XMLLanguage +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.undo.UndoManager +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.RangeMarker +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.impl.PsiDocumentManagerBase +import com.intellij.psi.xml.XmlAttributeValue +import com.intellij.psi.xml.XmlElement +import com.intellij.psi.xml.XmlFile +import com.intellij.psi.xml.XmlText +import com.intellij.util.containers.addIfNotNull +import com.intellij.util.containers.nullize +import com.intellij.util.containers.tail +import org.jetbrains.idea.maven.editor.MavenModelVersionSynchronizerService.Companion.SKIP_COMMAND_KEY +import org.jetbrains.idea.maven.editor.MavenModelVersionSynchronizerService.Companion.SYNCHRONIZER_KEY + +class MavenModelVersionSyncronizerImpl( + private val editor: EditorImpl, + private val project: Project, +) : DocumentListener, Disposable, CaretListener { + companion object { + private val MARKERS_KEY: Key> = Key("maven.version.synchronizer.markers") + } + + private var applying = false + private val pdm = PsiDocumentManager.getInstance(project) as PsiDocumentManagerBase + + fun listenForDocumentChanges() { + Disposer.register(editor.disposable, this) + editor.document.addDocumentListener(this, this) + val prev = editor.getUserData(SYNCHRONIZER_KEY) + if (prev != null && prev !== this) { + Disposer.dispose(prev) + } + editor.putUserData(SYNCHRONIZER_KEY, this) + } + + override fun dispose() { + editor.putUserData(SYNCHRONIZER_KEY, null) + } + + override fun caretRemoved(event: CaretEvent) { + val caret = event.getCaret() + clearMarkers(caret) + } + + override fun beforeDocumentChange(event: DocumentEvent) { + val document = event.document + val ideProject = project + if (applying + || ideProject.isDefault + || UndoManager.getInstance(ideProject).isUndoInProgress + || document.isInBulkUpdate) { + return + } + + if (document.getUserData(SKIP_COMMAND_KEY) == true) { + return + } + + val offset = event.offset + val oldLen = event.oldLength + val fragment = event.newFragment + val newLen = event.newLength + + + for (i in 0 until newLen) { + if (!isValidModelSymbols(fragment[i])) { + return + } + } + val caret = editor.caretModel.currentCaret + + var markers = getMarkers(caret) + + // if there are changes outside leader + markers?.let { + if (!fitsInLeader(it.first(), offset, oldLen)) { + markers = null + clearMarkers(caret) + } + } + + if (markers == null) { + if (pdm.synchronizer.isInSynchronization(document)) return + val psiFile = pdm.getPsiFile(document) as? XmlFile ?: return + + val leader = createLeaderMarker(psiFile, document, editor.caretModel.offset) ?: return + if (!fitsInLeader(leader, offset, oldLen)) return + + leader.rangeMarker.isGreedyToLeft = true + leader.rangeMarker.isGreedyToRight = true + + if (pdm.isUncommited(document)) { + pdm.commitDocument(document) + } + val supports = findSupports(leader, psiFile, document) ?: return + setMarkers(caret, leader, supports) + } + } + + private fun setMarkers(caret: Caret, leader: SynchronizationData, followers: List?) { + if (followers == null) { + clearMarkers(caret) + } + else { + caret.putUserData(MARKERS_KEY, listOf(leader) + followers) + } + } + + private fun getMarkers(caret: Caret): List? { + return caret.getUserData(MARKERS_KEY) + } + + private fun createLeaderMarker(psiFile: XmlFile, document: Document, offset: Int): SynchronizationData? { + val projectElement = psiFile.rootTag + val xmlnsElement = projectElement?.getAttribute("xmlns")?.valueElement + val schemaElement = projectElement?.getAttribute("xsi:schemaLocation")?.valueElement + val modelElement = projectElement?.findSubTags("modelVersion")?.firstOrNull()?.children?.filterIsInstance()?.first() + if (xmlnsElement?.textRange?.contains(offset) == true) return fromXmlns(xmlnsElement, document) + if (modelElement?.textRange?.contains(offset) == true) return fromModel(modelElement, document) + if (schemaElement?.textRange?.contains(offset) == true) return fromSchema(schemaElement, document)?.firstOrNull { it.rangeMarker.contains(offset) } + return null + } + + + private fun clearMarkers(caret: Caret) { + caret.putUserData(MARKERS_KEY, null) + } + + + private fun findSupports(leader: SynchronizationData, psiFile: XmlFile, document: Document): List? { + val leaderRange = leader.rangeMarker.textRange + val leaderElement = psiFile.getViewProvider().findElementAt(leader.rangeMarker.startOffset, XMLLanguage.INSTANCE) ?: return null + val projectElement = psiFile.rootTag + + val xmlnsElement = projectElement?.getAttribute("xmlns")?.valueElement + val schemaElement = projectElement?.getAttribute("xsi:schemaLocation")?.valueElement + val modelElement = projectElement?.findSubTags("modelVersion")?.firstOrNull()?.children?.filterIsInstance()?.first() + + if (!same(leaderElement, xmlnsElement) && !same(leaderElement, schemaElement) && !same(leaderElement, modelElement)) return null + val result = ArrayList() + result.addIfNotNull(fromXmlns(xmlnsElement, document)) + fromSchema(schemaElement, document)?.let { result.addAll(it) } + result.addIfNotNull(fromModel(modelElement, document)) + result.forEach { it.rangeMarker.isGreedyToLeft = true; it.rangeMarker.isGreedyToRight = true } + return result.filter { it.rangeMarker.isValid }.filterNot { it.rangeMarker.textRange.intersects(leaderRange) }.nullize() + } + + private fun same(leader: PsiElement?, supporter: XmlElement?): Boolean { + if (leader == null || supporter == null) return false + return leader === supporter || leader.parent === supporter + } + + private fun fromXmlns(xmlnsElement: XmlAttributeValue?, document: Document): SynchronizationData? { + if (xmlnsElement == null) return null + val pomRange = findRangeAfter(xmlnsElement, "maven.apache.org/POM/") + return pomRange?.let { document.createRangeMarker(it).synchronizedDots() } + + } + + private fun fromModel(modelElement: XmlText?, document: Document): SynchronizationData? { + if (modelElement == null) return null + return document.createRangeMarker(modelElement.textRange).synchronizedDots() + } + + + private fun fromSchema(schemaElement: XmlAttributeValue?, document: Document): List? { + if (schemaElement == null) return null + val result = ArrayList(2) + val pomRange = findRangeAfter(schemaElement, "maven.apache.org/POM/") + pomRange?.let { result.add(document.createRangeMarker(it).synchronizedDots()) }; + + val xsdRange = findRangeAfter(schemaElement, "maven.apache.org/xsd/maven-", ".xsd") + xsdRange?.let { result.add(document.createRangeMarker(it).synchronizedDots()) } + + val xsdRangeUnderscores = findRangeAfter(schemaElement, "maven.apache.org/maven-v", ".xsd") + xsdRangeUnderscores?.let { result.add(document.createRangeMarker(it).synchronizedUnderscore()) } + + return result.nullize() + } + + private fun findRangeAfter(schemaElement: XmlAttributeValue, needle: String, end: String = " "): TextRange? { + val value = schemaElement.value + val schemaTextRange = schemaElement.valueTextRange + + val foundIndex = value.indexOf(needle) + + if (foundIndex != -1) { + val endIndex = value.indexOf(end, foundIndex + needle.length) + val endIdx = if (endIndex != -1) endIndex else value.length + val start = schemaTextRange.startOffset + foundIndex + needle.length + val end = schemaTextRange.startOffset + endIdx + if (start <= end) return TextRange(start, end) + } + return null + } + + private fun isValidModelSymbols(ch: Char): Boolean { + return ch.isLetterOrDigit() || ch in setOf('.', '_') + } + + private fun fitsInLeader(leader: SynchronizationData, offset: Int, oldLength: Int): Boolean { + return leader.rangeMarker.isValid && offset >= leader.rangeMarker.startOffset && (offset + oldLength) <= leader.rangeMarker.endOffset + } + + fun performReplacement(caret: Caret) { + val markers = getMarkers(caret) ?: return + val document: Document = editor.document + val leader = markers.first() + val name = getValueToReplace(document, leader) + if (markers.any { !it.rangeMarker.isValid } || name == null) { + return + } + val apply = Runnable { + markers.tail().forEach { + document.replaceString(it.rangeMarker.startOffset, it.rangeMarker.endOffset, it.transformation(name)) + } + } + ApplicationManager.getApplication().runWriteAction { + val lookup = LookupManager.getActiveLookup(editor) as? LookupImpl + if (lookup != null) { + lookup.performGuardedChange(apply) + } + else { + apply.run() + } + } + } + + fun beforeCommandFinished() { + applying = true + try { + if (editor.caretModel.isIteratingOverCarets) { + performReplacement(editor.caretModel.getCurrentCaret()) + } + else { + editor.caretModel.runForEachCaret(::performReplacement) + } + } + finally { + applying = false + } + } + + private fun getValueToReplace(document: Document, leader: SynchronizationData): String? { + if (document.getTextLength() < leader.rangeMarker.getEndOffset()) { + return null + } + return document.getText(leader.rangeMarker.textRange) + } + +} + +private fun RangeMarker.synchronizedUnderscore(): SynchronizationData { + return SynchronizationData(this){ + it.replace('.', '_') + } +} + +private fun RangeMarker.synchronizedDots(): SynchronizationData { + return SynchronizationData(this){ + it.replace('_', '.') + } +} + +private data class SynchronizationData(val rangeMarker: RangeMarker, val transformation: (String) -> String) diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProject.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProject.kt index a824d83d1a19..21cd48134f9a 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProject.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProject.kt @@ -548,15 +548,6 @@ class MavenProject(val file: VirtualFile) { return myState.dependencyTree } - @Suppress("SpellCheckingInspection") - val supportedPackagings: Set - get() { - val result = mutableSetOf(MavenConstants.TYPE_POM, MavenConstants.TYPE_JAR, "ejb", "ejb-client", "war", "ear", "bundle", "maven-plugin") - for (each: MavenImporter in MavenImporter.getSuitableImporters(this)) { - each.getSupportedPackagings(result) - } - return result - } fun getDependencyTypesFromImporters(type: SupportedRequestType): Set { val res: MutableSet = HashSet() diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java index ea3606f1719c..b9125d4db588 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/project/MavenProjectsManager.java @@ -122,7 +122,6 @@ public abstract class MavenProjectsManager extends MavenSimpleProjectComponent } - @Override public void loadState(@NotNull MavenProjectsManagerState state) { myState = state; @@ -159,7 +158,7 @@ public abstract class MavenProjectsManager extends MavenSimpleProjectComponent @Deprecated(forRemoval = true) public File getLocalRepository() { - return MavenSettingsCache.getInstance(myProject).getEffectiveUserLocalRepo().toFile(); + return MavenSettingsCache.getInstance(myProject).getEffectiveUserLocalRepo().toFile(); } public Path getRepositoryPath() { @@ -254,6 +253,7 @@ public abstract class MavenProjectsManager extends MavenSimpleProjectComponent scheduleUpdateAllMavenProjects(MavenSyncSpec.full("MavenProjectsManager.onProjectStartup")); } } + private void initProjectsTree() { initLock.lock(); try { @@ -289,12 +289,13 @@ public abstract class MavenProjectsManager extends MavenSimpleProjectComponent Update update = new Update(this) { @Override public void run() { - saveTree(); + saveTree(); } }; if (MavenUtil.isMavenUnitTestModeEnabled()) { mySaveQueue.queue(update); - } else { + } + else { MergingQueueUtil.queueTracked(mySaveQueue, update); } } @@ -383,7 +384,9 @@ public abstract class MavenProjectsManager extends MavenSimpleProjectComponent if (!isInitialized()) { doInit(); doActivate(); - var distributionUrl = getWrapperDistributionUrl(ProjectUtil.guessProjectDir(myProject)); + var baseDir = ProjectUtil.guessProjectDir(myProject); + + var distributionUrl = baseDir == null ? null : getWrapperDistributionUrl(baseDir.toNioPath()); if (distributionUrl != null) { getGeneralSettings().setMavenHomeType(MavenWrapper.INSTANCE); } @@ -662,8 +665,8 @@ public abstract class MavenProjectsManager extends MavenSimpleProjectComponent } public void updateProjectTargetFolders() { - if (myProject.isDisposed()) return; - MavenProjectImporter.scheduleUpdateTargetFolders(myProject); + if (myProject.isDisposed()) return; + MavenProjectImporter.scheduleUpdateTargetFolders(myProject); } @ApiStatus.Internal diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistribution.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistribution.kt index 6fefd04d73ae..aecb8277ceac 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistribution.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistribution.kt @@ -1,6 +1,7 @@ // Copyright 2000-2021 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.maven.server +import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.io.FileUtil import org.jetbrains.idea.maven.utils.MavenUtil import java.nio.file.Path @@ -8,7 +9,7 @@ import java.nio.file.Path interface MavenDistribution { val name: String val mavenHome: Path - val version: String? + val version: @NlsSafe String? fun isValid(): Boolean fun compatibleWith(mavenDistribution: MavenDistribution): Boolean } diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistributionsCache.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistributionsCache.java index 04b4b7d44eac..b70939ee4f00 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistributionsCache.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenDistributionsCache.java @@ -50,6 +50,7 @@ public final class MavenDistributionsCache { } public void cleanCaches() { + MavenWrapperDownloader.invalidateCaches(); mySettingsDistribution.drop(); myWorkingDirToMultiModuleMap.clear(); myMultimoduleDirToWrapperedMavenDistributionsMap.clear(); @@ -107,6 +108,20 @@ public final class MavenDistributionsCache { return myVmSettingsMap.computeIfAbsent(multiModuleDir, MavenExternalParameters::readJvmConfigOptions); } + + public @NotNull MavenDistribution getMavenDistribution(@Nullable VirtualFile file) { + if (!useWrapper() || file == null) { + return mySettingsDistribution.getValue(); + } + + if (file.isDirectory()) { + return getMavenDistribution(file.getPath()); + } + else { + return getMavenDistribution(file.getParent()); + } + } + public @NotNull MavenDistribution getMavenDistribution(@Nullable String workingDirectory) { if (!useWrapper() || workingDirectory == null) { return mySettingsDistribution.getValue(); @@ -145,7 +160,8 @@ public final class MavenDistributionsCache { if (PluginManagerCore.isRunningFromSources()) { // running from sources Path mavenPath = mySourcePath.getValue(); return new LocalMavenDistribution(mavenPath, BundledMaven3.INSTANCE.getTitle()); - } else if (mavenPlugin != null) { // running with production classloading. Use maven3 folder inside maven plugin layout + } + else if (mavenPlugin != null) { // running with production classloading. Use maven3 folder inside maven plugin layout Path pathToBundledMaven = mavenPlugin.getPluginPath().resolve("lib").resolve("maven3"); return new LocalMavenDistribution(pathToBundledMaven, BundledMaven3.INSTANCE.getTitle()); } @@ -163,11 +179,12 @@ public final class MavenDistributionsCache { @Nullable String getWrapperDistributionUrl(String multimoduleDirectory) { - VirtualFile baseDir = LocalFileSystem.getInstance().findFileByPath(multimoduleDirectory); - if (baseDir == null) { - return null; - } - return MavenWrapperSupport.getWrapperDistributionUrl(baseDir); + return getWrapperDistributionUrl(Path.of(multimoduleDirectory)); + } + + @Nullable + String getWrapperDistributionUrl(Path multimoduleDirectory) { + return MavenWrapperSupport.getWrapperDistributionUrl(multimoduleDirectory); } private @NotNull String resolveMultiModuleDirectory(@NotNull String workingDirectory) { @@ -176,10 +193,10 @@ public final class MavenDistributionsCache { return FileUtilRt.toSystemIndependentName(calculateMultimoduleDirUpToFileTree(workingDirectory)); } return FileUtilRt.toSystemIndependentName(manager.getRootProjects().stream() - .map(MavenProject::getDirectory) - .filter(rpDirectory -> FileUtil.isAncestor(rpDirectory, workingDirectory, false)) - .findFirst() - .orElseGet(() -> calculateMultimoduleDirUpToFileTree(workingDirectory))); + .map(MavenProject::getDirectory) + .filter(rpDirectory -> FileUtil.isAncestor(rpDirectory, workingDirectory, false)) + .findFirst() + .orElseGet(() -> calculateMultimoduleDirUpToFileTree(workingDirectory))); } private @NotNull String calculateMultimoduleDirUpToFileTree(String directory) { diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperDownloader.java b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperDownloader.java index 47fc80185b2a..ac581deb1bbb 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperDownloader.java +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperDownloader.java @@ -17,6 +17,7 @@ import org.jetbrains.idea.maven.project.MavenWorkspaceSettings; import org.jetbrains.idea.maven.project.MavenWorkspaceSettingsComponent; import org.jetbrains.idea.maven.utils.MavenLog; +import java.nio.file.Path; import java.util.Locale; public final class MavenWrapperDownloader { @@ -31,6 +32,10 @@ public final class MavenWrapperDownloader { checkOrInstall(project, workingDir, MavenProjectsManager.getInstance(project).getSyncConsole(), showNotificationIfUrlMissing); } + public static void invalidateCaches() { + MavenWrapperMapping.getInstance().invalidate(); + } + private static synchronized void checkOrInstall(@NotNull Project project, @Nullable String workingDir, @Nullable MavenSyncConsole syncConsole, @@ -59,7 +64,7 @@ public final class MavenWrapperDownloader { try { distribution = new MavenWrapperSupport().downloadAndInstallMaven(distributionUrl, indicator, project); if (syncConsole != null && distributionUrl.toLowerCase(Locale.ENGLISH).startsWith("http:")) { - MavenWrapperSupport.showUnsecureWarning(syncConsole, LocalFileSystem.getInstance().findFileByPath(multiModuleDir)); + MavenWrapperSupport.showUnsecureWarning(syncConsole, Path.of(multiModuleDir)); } distributionsCache.addWrapper(multiModuleDir, distribution); if (syncConsole != null) syncConsole.finishWrapperResolving(null); diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperEventLogNotification.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperEventLogNotification.kt index a24944191648..877789575987 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperEventLogNotification.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperEventLogNotification.kt @@ -16,6 +16,7 @@ import org.jetbrains.idea.maven.execution.SyncBundle.message import org.jetbrains.idea.maven.project.MavenProjectBundle import org.jetbrains.idea.maven.server.MavenWrapperSupport.Companion.getWrapperProperties import org.jetbrains.idea.maven.statistics.MavenNotificationDisplayIds +import java.nio.file.Path class MavenWrapperEventLogNotification { companion object { @@ -23,7 +24,7 @@ class MavenWrapperEventLogNotification { fun noDistributionUrlEvent(project: Project, multiModuleDir: String) { val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Maven") ?: return ApplicationManager.getApplication().invokeLater { - val wrapperPropertyFile = LocalFileSystem.getInstance().findFileByPath(multiModuleDir)?.let { getWrapperProperties(it) } + val wrapperPropertyFile = LocalFileSystem.getInstance().findFileByPath(multiModuleDir)?.let { getWrapperProperties(it.toNioPath()) } if (wrapperPropertyFile == null) { wrapperPropertyFileNotFound(notificationGroup, project) } @@ -84,7 +85,8 @@ class MavenWrapperEventLogNotification { private fun distributionUrlEmpty(notificationGroup: NotificationGroup, project: Project, - wrapperPropertyFile: VirtualFile) { + wrapperPropertyFile: Path + ) { notificationGroup .createNotification( message("maven.wrapper.notification.title"), @@ -93,7 +95,7 @@ class MavenWrapperEventLogNotification { ) .setDisplayId(MavenNotificationDisplayIds.WRAPPER_EMPTY_URL) .addAction(NotificationAction.createSimple(message("maven.wrapper.notification.empty.url.action.check")) { - showFile(project, wrapperPropertyFile.toNioPath(), null) + showFile(project, wrapperPropertyFile, null) }) .addAction(NotificationAction.createSimple(message("maven.wrapper.notification.empty.url.action.disable")) { ShowSettingsUtil.getInstance().showSettingsDialog(project, diff --git a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperSupport.kt b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperSupport.kt index 0c83ea700302..415fa786ee9e 100644 --- a/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperSupport.kt +++ b/plugins/maven/src/main/java/org/jetbrains/idea/maven/server/MavenWrapperSupport.kt @@ -9,7 +9,6 @@ import com.intellij.openapi.externalSystem.util.environment.Environment import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.FileUtil -import com.intellij.openapi.vfs.VirtualFile import com.intellij.platform.eel.isPosix import com.intellij.platform.eel.provider.LocalEelDescriptor import com.intellij.platform.eel.provider.getEelDescriptor @@ -30,10 +29,7 @@ import java.nio.file.attribute.PosixFilePermissions import java.security.MessageDigest import java.util.* import java.util.concurrent.ConcurrentHashMap -import kotlin.io.path.exists -import kotlin.io.path.isDirectory -import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.name +import kotlin.io.path.* @State(name = "MavenWrapperMapping", storages = [Storage(value = "maven.wrapper.mapping.xml", roamingType = RoamingType.PER_OS)], @@ -52,6 +48,10 @@ internal class MavenWrapperMapping : PersistentStateComponent - + @@ -224,6 +225,14 @@ key="inspection.redundant.groupId.name" groupKey="inspection.group" enabledByDefault="true" level="WARNING" implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenRedundantGroupIdInspection"/> + + + + + + + + + + @@ -313,6 +342,12 @@ + + @@ -349,6 +384,10 @@ implementation="org.jetbrains.idea.maven.statistics.MavenLibraryCoordinatesWhitelistValidationRule"/> + + + @@ -997,5 +1036,10 @@ + + + diff --git a/plugins/maven/src/main/resources/inlayProviders/maven.pom.model.version.hints/preview.xml b/plugins/maven/src/main/resources/inlayProviders/maven.pom.model.version.hints/preview.xml new file mode 100644 index 000000000000..668783b34601 --- /dev/null +++ b/plugins/maven/src/main/resources/inlayProviders/maven.pom.model.version.hints/preview.xml @@ -0,0 +1,9 @@ + + + 4.1.0/*<# Compatible with Maven 4 #>*/ + group + artifact + 1.0 + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/Maven4RedundantParentCoordinates.html b/plugins/maven/src/main/resources/inspectionDescriptions/Maven4RedundantParentCoordinates.html new file mode 100644 index 000000000000..a6a9320b6f53 --- /dev/null +++ b/plugins/maven/src/main/resources/inspectionDescriptions/Maven4RedundantParentCoordinates.html @@ -0,0 +1,5 @@ + + +Reports redundant parent coordinates for Maven 4 projects + + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/MavenBomPackagingInOldSchema.html b/plugins/maven/src/main/resources/inspectionDescriptions/MavenBomPackagingInOldSchema.html new file mode 100644 index 000000000000..2075fa0c2f88 --- /dev/null +++ b/plugins/maven/src/main/resources/inspectionDescriptions/MavenBomPackagingInOldSchema.html @@ -0,0 +1,5 @@ + + +Ensures that the new Maven packaging does not appear in the legacy model version + + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/MavenModulesInMaven4.html b/plugins/maven/src/main/resources/inspectionDescriptions/MavenModulesInMaven4.html new file mode 100644 index 000000000000..4d75ec7cfd8a --- /dev/null +++ b/plugins/maven/src/main/resources/inspectionDescriptions/MavenModulesInMaven4.html @@ -0,0 +1,5 @@ + + +Reports the presence of the <modules> tag in the Maven 4.1.0 model version + + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/MavenNewModelVersionInOldMaven.html b/plugins/maven/src/main/resources/inspectionDescriptions/MavenNewModelVersionInOldMaven.html new file mode 100644 index 000000000000..0895df7a5ba0 --- /dev/null +++ b/plugins/maven/src/main/resources/inspectionDescriptions/MavenNewModelVersionInOldMaven.html @@ -0,0 +1,5 @@ + + +Reporst about old maven version in projects with model 4.1.0 + + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/MavenNoRootDefined.html b/plugins/maven/src/main/resources/inspectionDescriptions/MavenNoRootDefined.html new file mode 100644 index 000000000000..d271331e7c2c --- /dev/null +++ b/plugins/maven/src/main/resources/inspectionDescriptions/MavenNoRootDefined.html @@ -0,0 +1,5 @@ + + +Reports the absence of the .mvn directory/root tag, or when .mvn is located above the managed root project + + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/MavenRedundantVersion.html b/plugins/maven/src/main/resources/inspectionDescriptions/MavenRedundantVersion.html new file mode 100644 index 000000000000..3e8ea6dbd6f0 --- /dev/null +++ b/plugins/maven/src/main/resources/inspectionDescriptions/MavenRedundantVersion.html @@ -0,0 +1,5 @@ + + +Reports an unnecessary <version> definition, as it is already specified in the parent pom.xml + + \ No newline at end of file diff --git a/plugins/maven/src/main/resources/inspectionDescriptions/metaInformation.json b/plugins/maven/src/main/resources/inspectionDescriptions/metaInformation.json index 98a16f124a86..16ff5ef7e41c 100644 --- a/plugins/maven/src/main/resources/inspectionDescriptions/metaInformation.json +++ b/plugins/maven/src/main/resources/inspectionDescriptions/metaInformation.json @@ -12,6 +12,10 @@ "id": "MavenRedundantGroupId", "codeQualityCategory": "Sanity" }, + { + "id": "MavenRedundantVersion", + "codeQualityCategory": "Sanity" + }, { "id": "MavenDuplicatePluginInspection", "codeQualityCategory": "Sanity" @@ -20,6 +24,10 @@ "id": "MavenParentMissedVersionInspection", "codeQualityCategory": "Sanity" }, + { + "id": "Maven4RedundantParentCoordinates", + "codeQualityCategory": "Sanity" + }, { "id": "MavenPropertyInParent", "codeQualityCategory": "Reliability" @@ -31,6 +39,10 @@ { "id": "MavenNewElementsInOldSchema", "codeQualityCategory": "Reliability" + }, + { + "id": "MavenNoRootDefined", + "codeQualityCategory": "Reliability" } ] } \ No newline at end of file diff --git a/plugins/maven/src/main/resources/messages/MavenDomBundle.properties b/plugins/maven/src/main/resources/messages/MavenDomBundle.properties index 7189b9eec0ec..09876479eb0c 100644 --- a/plugins/maven/src/main/resources/messages/MavenDomBundle.properties +++ b/plugins/maven/src/main/resources/messages/MavenDomBundle.properties @@ -36,18 +36,30 @@ inspection.duplicate.dependencies.name=Duplicate Dependencies inspection.duplicate.plugin.declaration=Duplicate plugin declaration MavenDuplicateDependenciesInspection.has.duplicates=Dependency is duplicated in file(s)#treeend: {0} inspection.redundant.groupId.name=Redundant groupId +inspection.redundant.version.name=Redundant version +inspection.redundant.packaging.name=Redundant packaging +inspection.no.root.defined.name=Multi-module directory is not defined inspection.missed.parent.version=Parent version missed inspection.missed.parent.groupIdArtifactId=Parent groupId or artifactId is missing -inspection.redundant.groupId.fix=Remove unnecessary -inspection.redundant.groupId.fix.description=Definition of groupId is redundant, because it's inherited from the parent +inspection.redundant.element.fix=Remove unnecessary {0} +inspection.redundant.element.fix.description=Definition of {0} is redundant, because it''s inherited from the parent inspection.property.in.parent.name=Usage of properties in parent description inspection.property.in.parent.description=Properties in parent definition are prohibited fix.ignore.unresolved.maven.property=Ignore unresolved maven property +inspection.redundant.parent.coordinates.maven.4=The parent coordinates are redundant and not required in Maven 4 +inspection.redundant.parent.coordinates.maven.4.clean=Remove unnecessary tags +inspection.absence.root.dir.description=Multi-module directory should be defined for all maven projects +inspection.absence.dir.fix.create.dir=Create .mvn dir +inspection.absence.dir.fix.add.root=Add root="true" tag (Maven 4 only) + +inspection.modules.tag.in.maven.4=The tag is deprecated; use instead +inspection.modules.tag.in.maven.4.name=Change to + property.introduce.name=Name property.introduce.value=Value property.title.add=Add Maven Property @@ -66,9 +78,10 @@ maven.search.results.list.accessible.name=Search results maven.search.text.field.accessible.name=Search maven.dom.quickfix.add.maven.dependency=Add Maven Dependency maven.type.name.property=Property -inspection.message.unsupported.model.version.only.version.supported=Unsupported model version. Only versions {0} are supported. +inspection.message.unsupported.model.version.only.version.supported=Unsupported model version. Only versions {0} are supported in current Maven version ({1}) text.property=Property text.model.property=Model property +maven.version.tip=Maven version: {0} artifact.0.not.found=Artifact ''{0}'' not found dependency.0.not.found=Dependency ''{0}'' not found project.0.not.found=Project ''{0}'' not found @@ -84,4 +97,12 @@ plugin.model.doc.required=Required inspection.missed.model.version='modelVersion' child tag should be defined inspection.wrong.model.version=Wrong model version. Model version should be 4.1.0 for this tag -inspection.new.model.version.in.old.schema=Model version 4.1.0 is required for projects with 4.1.0 schema +inspection.new.model.version.in.old.schema=Model version 4.1.0 is required for projects with 4.1.0 schema +inspection.new.model.version.with.old.maven=Maven version 4+ is required for projects with 4.1.0 schema +inspection.new.packaging.in.old.model=Model version 4.1.0 is required for packaging {0} +inspection.new.packaging.in.old.model.name=Model version 4.1.0 is required for the packaging + +maven.version.inlay.hint.name=Maven Version +maven.version.inlay.hint.description=Shows Maven version for the model +maven.version.inlay.hint.text.3=Compatible with Maven 3 +maven.version.inlay.hint.text.4=Compatible with Maven 4 diff --git a/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties b/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties index bc92bb04516a..03828b45783b 100644 --- a/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties +++ b/plugins/maven/src/main/resources/messages/MavenSyncBundle.properties @@ -18,7 +18,7 @@ maven.sync.quickfixes.maven360=You use Maven 3.6.0, there could be issues import maven.sync.quickfixes.upgrade.to.jdk7=Maven version {0} requires at least JDK version 1.7 maven.sync.quickfixes.maven.version.title=Maven version issue -maven.sync.quickfixes.open.settings=Open Settings +maven.sync.quickfixes.update.maven.version=Update Maven version to {0} maven.sync.quickfixes.use.bundled.settings=Use Bundled {0} maven.sync.quickfixes.repository.blocked.title=Since Maven 3.8.1 http repositories are blocked diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenDomUtilTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenDomUtilTest.kt new file mode 100644 index 000000000000..b94cc48892f6 --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenDomUtilTest.kt @@ -0,0 +1,99 @@ +// 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.maven.dom + +import com.intellij.ide.highlighter.XmlFileType +import com.intellij.maven.testFramework.MavenTestCase +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory + + +class MavenDomUtilTest : MavenTestCase() { + + fun testIsProjectFileWithModel400() { + assertFalse(MavenDomUtil.isProjectFileWithModel410( + createXmlFile(""" + + + 4.0.0 + + test + test + 1 + + """.trimIndent()) + )) + } + + fun testIsProjectFileWithModel410() { + assertTrue(MavenDomUtil.isProjectFileWithModel410( + createXmlFile(""" + + + 4.1.0 + + test + test + 1 + + """.trimIndent()) + )) + } + + fun testIsProjectFileWithModel410Incomplete() { + assertFalse(MavenDomUtil.isProjectFileWithModel410( + createXmlFile(""" + + + 4.0.0 + test + test + 1 + + """.trimIndent()) + )) + } + + + fun testIsProjectFileWithModel410Incomplete2() { + assertFalse(MavenDomUtil.isProjectFileWithModel410( + createXmlFile(""" + + + 4.1.0 + + test + test + 1 + + """.trimIndent()) + )) + } + + fun testIsProjectFileWithModel410NoModel() { + assertTrue(MavenDomUtil.isProjectFileWithModel410( + createXmlFile(""" + + + + test + test + 1 + + """.trimIndent()) + )) + } + + fun createXmlFile(text: String) : PsiFile { + return PsiFileFactory.getInstance(project).createFileFromText("pom.xml", XmlFileType.INSTANCE, text); + } +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProviderTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProviderTest.kt new file mode 100644 index 000000000000..7abe6ed6ee93 --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelDocumentationProviderTest.kt @@ -0,0 +1,29 @@ +// 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.maven.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import com.intellij.openapi.application.readAction +import com.intellij.psi.xml.XmlFile +import kotlinx.coroutines.runBlocking +import org.junit.Test + +class MavenModelDocumentationProviderTest : MavenDomTestCase() { + + @Test + fun testModelVersionDocumentation() = runBlocking { + createProjectPom( + """ + test + project + 1 + """.trimIndent()) + + val provider = MavenModelDocumentationProvider() + val psi = findPsiFile(projectPom) as XmlFile + val doc = readAction { + provider.generateDoc(psi.rootTag!!.findSubTags("modelVersion").single(), null) + } + + assertEquals("Model property
project.modelVersion: $modelVersion
Maven version: ${getActualVersion(myMavenVersion!!)}", doc) + } +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelValidationTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelValidationTest.kt index 142632675c56..a84387a60fba 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelValidationTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenModelValidationTest.kt @@ -139,31 +139,6 @@ class MavenModelValidationTest : MavenDomWithIndicesTestCase() { checkHighlighting() } - @Test - fun testUnknownModelVersionMaven4() = runBlocking { - assumeMaven4() - fixture.saveText(projectPom, - """ - - 666 - foo - - """.trimIndent()) - checkHighlighting() - } - - @Test - fun testModelVersion41isUnsupportedInMaven3() = runBlocking { - assumeMaven3() - fixture.saveText(projectPom, - """ - - 4.1.0 - foo - - """.trimIndent()) - checkHighlighting() - } @Test fun testModelVersion41isSupportedInMaven4() = runBlocking { diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPackagingCompletionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPackagingCompletionTest.kt index 1ad70a5cec0c..1bee70213abf 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPackagingCompletionTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPackagingCompletionTest.kt @@ -35,7 +35,7 @@ class MavenPackagingCompletionTest : MavenDomTestCase() { """.trimIndent()) - assertCompletionVariants(projectPom, "jar", "pom", "war", "ejb", "ejb-client", "ear", "bundle", "maven-plugin") + assertCompletionVariants(projectPom, *arrayOfNotNull("jar", "pom", "war", "ejb", "ejb-client", "ear", "bundle", "maven-plugin", withModel410Only("bom"))) } @Test diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPropertyCompletionAndResolutionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPropertyCompletionAndResolutionTest.kt index af13dd9e121f..efc749d7d78a 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPropertyCompletionAndResolutionTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/dom/MavenPropertyCompletionAndResolutionTest.kt @@ -1207,6 +1207,165 @@ class MavenPropertyCompletionAndResolutionTest : MavenDomTestCase() { assertCompletionVariantsDoNotInclude(projectPom, "project.groupId") } + + @Test + fun testCompletingMaven4Specific() = runBlocking { + assumeModel_4_1_0("applicable for maven4") + updateProjectPom(""" + test + project + 1 + ${'$'}{project. + """.trimIndent()) + + val variants = getCompletionVariants(projectPom) + assertContain(variants, "project.rootDirectory") + + updateProjectPom(""" + test + project + 1 + ${'$'}{session. + """.trimIndent()) + val sessionVariants = getCompletionVariants(projectPom) + assertContain(sessionVariants, "session.rootDirectory", "session.topDirectory") + } + + @Test + fun testResolveMaven4SpecificRootDir() = runBlocking { + assumeModel_4_1_0("applicable for maven4") + + updateProjectPom(""" + test + project + 1 + + ${'$'}{project.rootDirectory} + + + """.trimIndent()) + + val rootDirectory = readAction { PsiManager.getInstance(project).findDirectory(projectPom.getParent())!! } + assertResolved(projectPom, rootDirectory) + } + + @Test + fun testResolveMaven4SpecificRootDirForSubmodules() = runBlocking { + assumeModel_4_1_0("applicable for maven4") + createModulePom("m1", + """ + + test + project + 1 + + m1 + """.trimIndent()) + updateProjectPom(""" + test + project + 1 + pom + + m1 + + """.trimIndent()) + updateAllProjects() + + val m1 = updateModulePom("m1", + """ + + test + project + 1 + + m1 + + ${'$'}{project.rootDirectory} + + """.trimIndent()) + val rootDirectory = readAction { PsiManager.getInstance(project).findDirectory(projectPom.getParent()) } + assertResolved(m1, rootDirectory!!) + } + + @Test + fun testResolveMaven4SpecificSessionRootDirForSubmodules() = runBlocking { + assumeModel_4_1_0("applicable for maven4") + createModulePom("m1", + """ + + test + project + 1 + + m1 + """.trimIndent()) + updateProjectPom(""" + test + project + 1 + pom + + m1 + + """.trimIndent()) + updateAllProjects() + + val m1 = updateModulePom("m1", + """ + + test + project + 1 + + m1 + + ${'$'}{session.rootDirectory} + + """.trimIndent()) + val rootDirectory = readAction { PsiManager.getInstance(project).findDirectory(projectPom.getParent()) } + assertResolved(m1, rootDirectory!!) + } + + @Test + fun testResolveMaven4SpecificSessionTopDirForSubmodules() = runBlocking { + assumeModel_4_1_0("applicable for maven4") + createModulePom("m1", + """ + + test + project + 1 + + m1 + """.trimIndent()) + updateProjectPom(""" + test + project + 1 + pom + + m1 + + """.trimIndent()) + updateAllProjects() + + val m1 = updateModulePom("m1", + """ + + test + project + 1 + + m1 + + ${'$'}{session.topDirectory} + + """.trimIndent()) + val rootDirectory = readAction { PsiManager.getInstance(project).findDirectory(projectPom.getParent()) } + assertResolved(m1, rootDirectory!!) + } + private suspend fun readWithProfiles(vararg profiles: String) { projectsManager.explicitProfiles = MavenExplicitProfiles(listOf(*profiles)) updateAllProjects() diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/editor/MavenModelSynchronizerTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/editor/MavenModelSynchronizerTest.kt new file mode 100644 index 000000000000..332b6d9dc5bd --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/editor/MavenModelSynchronizerTest.kt @@ -0,0 +1,163 @@ +// 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.maven.editor + +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class MavenModelSynchronizerTest : BasePlatformTestCase() { + @Throws(Exception::class) + override fun setUp() { + super.setUp() + myFixture.setCaresAboutInjection(false) + registerSyncSynchronization() + } + + private fun registerSyncSynchronization() { + EditorFactory.getInstance().addEditorFactoryListener(object : EditorFactoryListener { + override fun editorCreated(event: EditorFactoryEvent) { + if (event.editor.project === project) { + val editor = event.editor as? EditorImpl ?: return + MavenModelVersionSyncronizerImpl(editor, project).listenForDocumentChanges() + } + } + + }, testRootDisposable) + } + + fun testTypingInModel() { + + doTest(""" + + 4.0.0 + test + test + 1 +""", "\b1", """ + + 4.1.0 + test + test + 1 +""") + } + + fun testTypingInModeWithUnderscoresSchemaLocation() { + + doTest(""" + + 4.0.0 + test + test + 1 +""", "\b1", """ + + 4.1.0 + test + test + 1 +""") + } + + fun testTypingInUnderscoresSchemaLocation() { + + doTest(""" + + 4.0.0 + test + test + 1 +""", "\b1", """ + + 4.1.0 + test + test + 1 +""") + } + + fun testTypingInXmlns() { + doTest(""" + + 4.0.0 + test + test + 1 +""", "\b1", """ + + 4.1.0 + test + test + 1 +""") + } + + fun testTypingInSchemaLocationPom() { + doTest(""" + + 4.0.0 + test + test + 1 +""", "\b1", """ + + 4.1.0 + test + test + 1 +""") + } + + fun testTypingInSchemaLocationXsd() { + doTest(""" + + 4.0.0 + test + test + 1 +""", "\b1", """ + + 4.1.0 + test + test + 1 +""") + } + + fun doTest(content: String, toType: String, expected: String) { + val xmlFileType = FileTypeManager.getInstance().getFileTypeByExtension("xml") + myFixture.configureByText(xmlFileType, content) + myFixture.type(toType) + myFixture.checkResult(expected) + } + +} + + diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/Maven4RedundantParentCoordinatesInspectionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/Maven4RedundantParentCoordinatesInspectionTest.kt new file mode 100644 index 000000000000..ffbd1268459a --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/Maven4RedundantParentCoordinatesInspectionTest.kt @@ -0,0 +1,143 @@ +// 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.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.Maven4RedundantParentCoordinatesInspection +import org.junit.Test + +class Maven4RedundantParentCoordinatesInspectionTest : MavenDomTestCase() { + override fun setUp() { + super.setUp() + + fixture.enableInspections(Maven4RedundantParentCoordinatesInspection::class.java) + } + + @Test + fun testDoNotFireHighlightInMaven3() = runBlocking { + assumeMaven3() + val moduleFile = createModulePom("m1", """ + + test + test + 1 + + m1 +""") + + importProjectAsync(""" + test + test + 1 + pom + + m1 + +""") + checkHighlighting(moduleFile) + } + + + @Test + fun testFireHighlightInMaven4() = runBlocking { + assumeMaven4() + val moduleFile = createModulePom("m1", """ + + test + test + 1 + + m1 +""") + + importProjectAsync(""" + test + test + 1 + pom + + m1 + +""") + checkHighlighting(moduleFile) + } + + @Test + fun testDoQuickFixForParent() = runBlocking { + assumeMaven4() + val moduleFile = createModulePom("m1", """ + + test + test + 1 + + m1 +""") + + importProjectAsync(""" + test + test + 1 + pom + + m1 + +""") + checkHighlighting(moduleFile) + + val intention = fixture.availableIntentions.singleOrNull{it.text == "Remove unnecessary tags"} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(createPomXml(""" + + m1 +"""), true) + } + + @Test + fun testDoQuickFixForParentWithRelativePath() = runBlocking { + assumeMaven4() + val module1File = createModulePom("m1", """ + test + 1 + pom + m1 +""") + + val module2File = createModulePom("m2", """ + + test + m1 + 1 + ../m1/pom.xml + + m2 +""") + + importProjectAsync(""" + test + test + 1 + pom + + m1 + m2 + +""") + checkHighlighting(module2File) + + val intention = fixture.availableIntentions.singleOrNull{it.text == "Remove unnecessary tags"} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(createPomXml(""" + + ../m1/pom.xml + + m2 +"""), true) + } + + +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenBomInOldModelInspectionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenBomInOldModelInspectionTest.kt new file mode 100644 index 000000000000..0e9d9248cf90 --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenBomInOldModelInspectionTest.kt @@ -0,0 +1,80 @@ +// 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.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.MavenBomPackagingInOldSchema +import org.junit.Test + +class MavenBomInOldModelInspectionTest : MavenDomTestCase() { + override fun setUp() { + super.setUp() + + fixture.enableInspections(MavenBomPackagingInOldSchema::class.java) + runBlocking { + importProjectAsync(""" + test + test + 1.0 + """.trimIndent()) + } + } + + @Test + fun testDoNotFireHighlightIn41() = runBlocking { + assumeModel_4_1_0("") + createProjectPom(""" + my.group + childA + 1.0 + bom + """.trimIndent()) + + checkHighlighting() + } + + @Test + fun testFireHighlightIn40() = runBlocking { + assumeModel_4_0_0("") + setRawPomFile(""" + + 4.0.0 + my.group + artifact + 1.0 + bom + + """.trimIndent()) + checkHighlighting() + } + + @Test + fun testUpdateToModel41() = runBlocking { + assumeModel_4_0_0("") + setRawPomFile(""" + + 4.0.0 + my.group + childA + 1.0 + bom + """.trimIndent()) + checkHighlighting() + val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Update Maven Model and XSD to 4.1.0")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + fixture.checkResult(""" + + 4.1.0 + my.group + childA + 1.0 + bom """.trimIndent()) + } +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenModulesInMaven4InspectionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenModulesInMaven4InspectionTest.kt new file mode 100644 index 000000000000..0a9fe7eb09b2 --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenModulesInMaven4InspectionTest.kt @@ -0,0 +1,76 @@ +// 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.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.MavenModulesInMaven4Inspection +import org.junit.Test + +class MavenModulesInMaven4InspectionTest : MavenDomTestCase() { + override fun setUp() { + super.setUp() + fixture.enableInspections(MavenModulesInMaven4Inspection::class.java) + } + + @Test + fun shouldHighlight() = runBlocking { + assumeModel_4_1_0("applicable only for model 4.1.0") + createModulePom("m1", """ + test + m1 + test +""") + + createModulePom("m2", """ + test + m2 + test +""") + createModulePom("m1/sub1", """ + test + sub1 + test +""") + importProjectAsync(""" + test + test + test + + m1 + m2 + m1/sub1 + +""") + createProjectPom(""" + test + test + test + + m1 + m2 + m1/sub1 + +""") + checkHighlighting() + + val intention = fixture.availableIntentions.singleOrNull { it.text == "Replace to " } + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(""" + + 4.1.0 + + test + test + test + + m1 + m2 + m1/sub1 + +""") + } +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionInOldSchemaInspectionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionInOldSchemaInspectionTest.kt new file mode 100644 index 000000000000..e0ecf08b39fc --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionInOldSchemaInspectionTest.kt @@ -0,0 +1,176 @@ +// 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.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.MavenNewModelVersionInOldSchemaInspection +import org.junit.Test + +class MavenNewModelVersionInOldSchemaInspectionTest: MavenDomTestCase() { + override fun setUp() { + super.setUp() + + + fixture.enableInspections(MavenNewModelVersionInOldSchemaInspection::class.java) + runBlocking { + importProjectAsync(""" + test + test + 1.0 + """.trimIndent()) + } + } + + @Test + fun testCheckHighlightingWrongModel() = runBlocking{ + assumeModel_4_0_0("testing only for model 4.0.0") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + } + + @Test + fun testCheckNotHighlightingModel4_0() = runBlocking{ + assumeModel_4_0_0("testing only for model 4.0.0") + setRawPomFile(""" + + 4.0.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + } + + @Test + fun testCheckNotHighlightingModel4_1() = runBlocking{ + assumeModel_4_1_0("testing only for model 4.1.0") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + } + + @Test + fun testCheckNotHighlightingModel4_1_underscore() = runBlocking{ + assumeModel_4_1_0("testing only for model 4.1.0") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + } + + @Test + fun testCheckQuickFix() = runBlocking{ + assumeModel_4_0_0("testing only for model 4.0.0") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + + val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Update Maven Model and XSD to 4.1.0")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + } + + + @Test + fun testCheckQuickFixAddsXMLSchemaInstance() = runBlocking{ + assumeModel_4_0_0("testing only for model 4.0.0") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + + val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Update Maven Model and XSD to 4.1.0")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + } + + + @Test + fun testQuickFixLeavesHttpsIfWasDefined() = runBlocking{ + assumeModel_4_0_0("testing only for model 4.0.0") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + + val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Update Maven Model and XSD to 4.1.0")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + } + +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionWithOldMavenInspectionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionWithOldMavenInspectionTest.kt new file mode 100644 index 000000000000..a339f0711df1 --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNewModelVersionWithOldMavenInspectionTest.kt @@ -0,0 +1,48 @@ +// 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.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.MavenNewModelVersionInOldMavenInspection +import org.junit.Test + +class MavenNewModelVersionWithOldMavenInspectionTest : MavenDomTestCase() { + override fun setUp() { + super.setUp() + + fixture.enableInspections(MavenNewModelVersionInOldMavenInspection::class.java) + } + + @Test + fun testCheckNoHighlightingInNewMaven() = runBlocking { + assumeModel_4_1_0("not applicable for old model version") + importProjectAsync(""" + test + test + test +""") + checkHighlighting() + } + + @Test + fun testCheckHighlightingInOldMaven() = runBlocking { + assumeMaven3() + importProjectAsync(""" + test + test + test +""") + setRawPomFile(""" + + 4.1.0 + my.group + artifact + 1.0 + + """.trimIndent()) + checkHighlighting() + } + +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNoRootDefinedInspectionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNoRootDefinedInspectionTest.kt new file mode 100644 index 000000000000..9043d2b33335 --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenNoRootDefinedInspectionTest.kt @@ -0,0 +1,139 @@ +// 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.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.MavenNoRootDefinedInspection +import org.junit.Test +import kotlin.io.path.isDirectory + +class MavenNoRootDefinedInspectionTest : MavenDomTestCase() { + override fun setUp() { + super.setUp() + + fixture.enableInspections(MavenNoRootDefinedInspection::class.java) + } + + @Test + fun testHighlightingWithMvn() = runBlocking { + importProjectAsync(""" + my.group + childA + 1.0 + """.trimIndent()) + createProjectSubDir(".mvn") + checkHighlighting() + } + + @Test + fun testHighlightingWithoutMvn() = runBlocking { + importProjectAsync(""" + my.group + childA + 1.0 + """.trimIndent()) + + setRawPomFile(""" + + + $modelVersion +my.group +childA +1.0 + """.trimIndent()) + + checkHighlighting() + } + + @Test + fun testHighlightingWithRoot() = runBlocking { + assumeModel_4_1_0("only for model 4.1.0") + importProjectAsync(""" + my.group + childA + 1.0 + """.trimIndent()) + + setRawPomFile(""" + + + $modelVersion + my.group + childA + 1.0 + """.trimIndent()) + checkHighlighting() + } + + + @Test + fun testRootTagQuickFix() = runBlocking { + assumeModel_4_1_0("applicable for model version 4.1.0") + importProjectAsync(""" + my.group + childA + 1.0 + """.trimIndent()) + val mvnDir = projectRoot.toNioPath().resolve(".mvn") + assertFalse("Directory should not exist!", mvnDir.isDirectory()) + + setRawPomFile(""" + <project xmlns="http://maven.apache.org/POM/$modelVersion" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/$modelVersion http://maven.apache.org/xsd/maven-$modelVersion.xsd"> + $modelVersion + my.group + childA + 1.0 + """.trimIndent()) + checkHighlighting() + val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Add root")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(""" + + 4.1.0 + my.group + childA + 1.0 + """.trimIndent()) + } + + @Test + fun testMvnDirectoryQuickFix() = runBlocking { + + importProjectAsync(""" + my.group + childA + 1.0 + """.trimIndent()) + val mvnDir = projectRoot.toNioPath().resolve(".mvn") + assertFalse("Directory should not exist!", mvnDir.isDirectory()) + + setRawPomFile(""" + <project xmlns="http://maven.apache.org/POM/$modelVersion" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/$modelVersion http://maven.apache.org/xsd/maven-$modelVersion.xsd"> + $modelVersion + my.group + childA + 1.0 + """.trimIndent()) + checkHighlighting() + val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Create .mvn dir")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + assertTrue("Directory should be created", mvnDir.isDirectory()) + } + + +} \ No newline at end of file diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantGroupIdTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantGroupIdTest.kt index b56cdd9e3194..0bf867e9aadb 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantGroupIdTest.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantGroupIdTest.kt @@ -11,6 +11,14 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() { super.setUp() fixture.enableInspections(MavenRedundantGroupIdInspection::class.java) + + runBlocking { + importProjectAsync(""" + test + test + 1.0 + """.trimIndent()) + } } @Test @@ -31,11 +39,11 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() { childA 1.0 - - my.group - parent - 1.0 - + + my.group + parent + 1.0 + """.trimIndent()) checkHighlighting() @@ -49,10 +57,10 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() { 1.0 - my.group - parent - 1.0 - + my.group + parent + 1.0 + """.trimIndent()) checkHighlighting() @@ -75,12 +83,9 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() { fixture.configureFromExistingVirtualFile(projectPom) fixture.doHighlighting() - for (intention in fixture.availableIntentions) { - if (intention.text.startsWith("Remove ") && intention.text.contains("")) { - fixture.launchAction(intention) - break - } - } + val intention = fixture.availableIntentions.singleOrNull{it.text.startsWith("Remove ") && it.text.contains("groupId")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) //doPostponedFormatting(myProject) diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantVersionTest.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantVersionTest.kt new file mode 100644 index 000000000000..60d279b0485f --- /dev/null +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/inspections/dom/MavenRedundantVersionTest.kt @@ -0,0 +1,97 @@ +package org.jetbrains.idea.maven.inspections.dom + +import com.intellij.maven.testFramework.MavenDomTestCase +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.dom.inspections.MavenRedundantVersionInspection +import org.junit.Test + +class MavenRedundantVersionTest : MavenDomTestCase() { + override fun setUp() { + super.setUp() + + fixture.enableInspections(MavenRedundantVersionInspection::class.java) + + runBlocking { + importProjectAsync(""" + test + test + 1.0 + """.trimIndent()) + } + } + + @Test + fun testHighlighting1() = runBlocking { + createProjectPom(""" + my.group + childA + 1.0 + """.trimIndent()) + + checkHighlighting() + } + + @Test + fun testHighlighting2() = runBlocking { + createProjectPom(""" + childGroupId + childA + 2.0 + + + my.group + parent + 1.0 + + """.trimIndent()) + + checkHighlighting() + } + + @Test + fun testHighlighting3() = runBlocking { + createProjectPom(""" + childA + 1.0 + + + my.group + parent + 1.0 + + """.trimIndent()) + + checkHighlighting() + } + + @Test + fun testQuickFix() = runBlocking { + createProjectPom(""" + childA + mavenParent + >1.0 + + mavenParent + mavenParent + 1.0 + + """.trimIndent()) + + fixture.configureFromExistingVirtualFile(projectPom) + fixture.doHighlighting() + + val intention = fixture.availableIntentions.singleOrNull{it.text.startsWith("Remove ") && it.text.contains("version")} + assertNotNull("Cannot find intention", intention) + fixture.launchAction(intention!!) + + fixture.checkResult(createPomXml(""" + childA + mavenParent + + mavenParent + mavenParent + 1.0 + + """.trimIndent())) + } +} diff --git a/plugins/maven/src/test/java/org/jetbrains/idea/maven/quickfixes/UpdateXmlTo410Test.kt b/plugins/maven/src/test/java/org/jetbrains/idea/maven/quickfixes/UpdateXmlTo410Test.kt index 50b99c7163dd..7da634ecb26f 100644 --- a/plugins/maven/src/test/java/org/jetbrains/idea/maven/quickfixes/UpdateXmlTo410Test.kt +++ b/plugins/maven/src/test/java/org/jetbrains/idea/maven/quickfixes/UpdateXmlTo410Test.kt @@ -9,7 +9,7 @@ import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase import kotlinx.coroutines.runBlocking import org.jetbrains.idea.maven.buildtool.quickfix.UpdateXmlsTo410 import org.jetbrains.idea.maven.model.MavenConstants -import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XLMNS +import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XMLNS import org.jetbrains.idea.maven.model.MavenConstants.MAVEN_4_XSD import org.mockito.Mockito.mock import org.mockito.Mockito.`when` @@ -42,9 +42,9 @@ class UpdateXmlsTo410Test : LightJavaCodeInsightFixtureTestCase() { readAction { val updatedProjectTag = xmlFile.document?.rootTag - assertEquals(MavenConstants.MAVEN_4_XLMNS, updatedProjectTag?.getAttribute("xmlns")?.value) + assertEquals(MavenConstants.MAVEN_4_XMLNS, updatedProjectTag?.getAttribute("xmlns")?.value) assertEquals("http://www.w3.org/2001/XMLSchema-instance", updatedProjectTag?.getAttribute("xmlns:xsi")?.value) - assertEquals("$MAVEN_4_XLMNS $MAVEN_4_XSD", updatedProjectTag?.getAttribute("xsi:schemaLocation")?.value) + assertEquals("$MAVEN_4_XMLNS $MAVEN_4_XSD", updatedProjectTag?.getAttribute("xsi:schemaLocation")?.value) assertEquals(MavenConstants.MODEL_VERSION_4_1_0, updatedProjectTag?.findFirstSubTag("modelVersion")?.value?.text) } diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenDomTestCase.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenDomTestCase.kt index ae2ae1c8402b..cf7e5e710efc 100644 --- a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenDomTestCase.kt +++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenDomTestCase.kt @@ -736,4 +736,5 @@ abstract class MavenDomTestCase : MavenMultiVersionImportingTestCase() { } + } \ No newline at end of file diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenMultiVersionImportingTestCase.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenMultiVersionImportingTestCase.kt index 65a632feebe2..463580114a1b 100644 --- a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenMultiVersionImportingTestCase.kt +++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenMultiVersionImportingTestCase.kt @@ -468,7 +468,8 @@ abstract class MavenMultiVersionImportingTestCase : MavenImportingTestCase() { }.toList() } - internal fun getActualVersion(version: String): String { + @JvmStatic + protected fun getActualVersion(version: String): String { if (version == "bundled") { return MavenDistributionsCache.resolveEmbeddedMavenHome().version!! } diff --git a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenTestCase.kt b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenTestCase.kt index 9d80583a3c2d..d45773568349 100644 --- a/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenTestCase.kt +++ b/plugins/maven/testFramework/src/com/intellij/maven/testFramework/MavenTestCase.kt @@ -758,6 +758,13 @@ abstract class MavenTestCase : UsefulTestCase() { assumeTrue("Unable to run the test in non-local environment: $cause", LocalEelDescriptor == project.getEelDescriptor()) } + protected fun setRawPomFile(content: String) { + Files.write(projectPom.toNioPath(), content.toByteArray(StandardCharsets.UTF_8)) + projectRoot.refresh(false, false) + val f = projectRoot.findChild("pom.xml") ?: throw AssertionError("can't find pom.xml in vfs") + refreshFiles(listOf(f)) + } + companion object { @Language("XML") fun createPomXml(modelVersion: String, @Language(value = "XML", prefix = "", suffix = "") xml: @NonNls String?): @NonNls String {