[maven] [IDEA-381098] maven4 quick fixes

Merge-request: IJ-MR-175311
Merged-by: Aleksandr Bubenchikov <alexander.bubenchikov@jetbrains.com>

GitOrigin-RevId: e3cd28795c7a24db59fd581d93843f72bfea3278
This commit is contained in:
Alexander Bubenchikov
2025-10-25 08:37:47 +00:00
committed by intellij-monorepo-bot
parent d7ef6ee9d4
commit 0581439bbb
58 changed files with 2661 additions and 234 deletions
@@ -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";
@@ -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) }
}
}
}
}
@@ -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
@@ -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
@@ -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<String> {
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 {
@@ -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<String> getValues(@NotNull ConvertContext context, @NotNull MavenProject project) {
return project.getSupportedPackagings();
}
}
@@ -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<String> {
val result = mutableSetOf<String>()
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<String> = setOf(MavenConstants.TYPE_POM, MavenConstants.TYPE_JAR, "ejb", "ejb-client", "war", "ear", "bundle", "maven-plugin")
val MAVEN_4_SPECIFIC: Set<String> = setOf("bom")
}
}
@@ -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"))
}
}
}
}
@@ -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<ProblemDescriptor?>? {
if (file is XmlFile && file.isPhysical()) {
val model =
DomManager.getDomManager(file.getProject()).getFileElement<MavenDomProjectModel?>(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
}
@@ -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<ProblemDescriptor?>? {
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()
}
}
}
}
@@ -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?>(MavenDomProjectModel::class.java) {
override fun getGroupDisplayName(): String {
return MavenDomBundle.message("inspection.group")
}
override fun getDefaultLevel(): HighlightDisplayLevel {
return HighlightDisplayLevel.ERROR
}
override fun checkFileElement(
domFileElement: DomFileElement<MavenDomProjectModel?>,
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()
)
}
}
@@ -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<ProblemDescriptor?>? {
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("<subprojects/>")
modulesList.forEach {
val tag = factory.createTagFromText("<subproject>$it</subproject>")
subprojectsTag.addSubTag(tag, false)
}
model.modules.xmlTag?.replace(subprojectsTag)
}
}
}
}
@@ -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?>(MavenDomProjectModel::class.java) {
override fun getGroupDisplayName(): String {
return MavenDomBundle.message("inspection.group")
}
override fun getDefaultLevel(): HighlightDisplayLevel {
return HighlightDisplayLevel.ERROR
}
override fun checkFileElement(
domFileElement: DomFileElement<MavenDomProjectModel?>,
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 """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>${mavenProject.mavenId.groupId}</groupId>
<artifactId>${mavenProject.mavenId.artifactId}</artifactId>
<version>${mavenProject.mavenId.version}</version>
</project>
"""
}
}
@@ -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?>(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<ProblemDescriptor?>? {
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<MavenDomProjectModel?>(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")
}
}
}
@@ -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<MavenDomProjectModel> 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();
}
}
}
@@ -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
}
@@ -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
}
@@ -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 <a href="https://www.mojohaus.org/build-helper-maven-plugin/parse-version-mojo.html#propertyPrefix">mojohaus documentation</a>
*/
private @Nullable PsiElement resolveAsParsedVersion(@NotNull String propertyText, @NotNull MavenProject mavenProject) {
@@ -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<MavenModelVersionSyncronizerImpl> = Key.create("maven.version.xml.sync")
@JvmField
internal val SKIP_COMMAND_KEY: Key<Boolean> = Key.create("maven.version.synchronizer.skip.command")
}
private val writeLock = Semaphore(1)
private fun recreateSynchronizersFor(editors: List<EditorImpl>) {
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<MavenModelVersionSyncronizerImpl> =
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<MavenModelVersionSynchronizerService>()?.recreateSynchronizersFor(editors.filterIsInstance<EditorImpl>())
}
}
}
}
class MavenModelVersionEditorFactoryListener : EditorFactoryListener {
override fun editorCreated(event: EditorFactoryEvent) {
val editor = event.editor
val project = editor.project
project?.service<MavenModelVersionSynchronizerService>()?.scheduleEnsureSynchronizerCreated(editor as? EditorImpl ?: return)
}
}
@@ -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<List<SynchronizationData>> = 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<SynchronizationData>?) {
if (followers == null) {
clearMarkers(caret)
}
else {
caret.putUserData(MARKERS_KEY, listOf(leader) + followers)
}
}
private fun getMarkers(caret: Caret): List<SynchronizationData>? {
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<XmlText>()?.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<SynchronizationData>? {
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<XmlText>()?.first()
if (!same(leaderElement, xmlnsElement) && !same(leaderElement, schemaElement) && !same(leaderElement, modelElement)) return null
val result = ArrayList<SynchronizationData>()
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<SynchronizationData>? {
if (schemaElement == null) return null
val result = ArrayList<SynchronizationData>(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)
@@ -548,15 +548,6 @@ class MavenProject(val file: VirtualFile) {
return myState.dependencyTree
}
@Suppress("SpellCheckingInspection")
val supportedPackagings: Set<String>
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<String> {
val res: MutableSet<String> = HashSet()
@@ -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
@@ -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
}
@@ -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) {
@@ -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);
@@ -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,
@@ -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<MavenWrapperMappin
myState.mapping.putAll(state.mapping)
}
fun invalidate() {
myState.mapping.clear()
}
companion object {
@JvmStatic
fun getInstance(): MavenWrapperMapping {
@@ -74,7 +74,7 @@ internal class MavenWrapperSupport {
indicator?.apply { text = SyncBundle.message("maven.sync.wrapper.downloading.from", urlString) }
try {
HttpRequests.request(urlString)
.tuner{
.tuner {
val username = Environment.getVariable("MVNW_USERNAME")
val password = Environment.getVariable("MVNW_PASSWORD")
if (!username.isNullOrBlank() && !password.isNullOrBlank()) {
@@ -177,6 +177,7 @@ internal class MavenWrapperSupport {
}
}
private fun getZipFile(distributionUrl: String, project: Project): Path {
val baseName: String = getDistName(distributionUrl)
val distName: String = FileUtil.getNameWithoutExtension(baseName)
@@ -208,13 +209,13 @@ internal class MavenWrapperSupport {
private val DISTRIBUTION_URL_PROPERTY = "distributionUrl"
@JvmStatic
fun getWrapperDistributionUrl(baseDir: VirtualFile?): String? {
fun getWrapperDistributionUrl(baseDir: Path?): String? {
try {
val wrapperProperties = getWrapperProperties(baseDir) ?: return null
val properties = Properties()
val stream = ByteArrayInputStream(wrapperProperties.contentsToByteArray(true))
val stream = ByteArrayInputStream(wrapperProperties.readBytes())
properties.load(stream)
val configuredProperty = properties.getProperty(DISTRIBUTION_URL_PROPERTY)
val urlBase = Environment.getVariable("MVNW_REPOURL")
@@ -233,13 +234,13 @@ internal class MavenWrapperSupport {
}
@JvmStatic
fun showUnsecureWarning(console: MavenSyncConsole, mavenProjectMultimodulePath: VirtualFile?) {
fun showUnsecureWarning(console: MavenSyncConsole, mavenProjectMultimodulePath: Path?) {
val properties = getWrapperProperties(mavenProjectMultimodulePath)
val line = properties?.inputStream?.bufferedReader(properties.charset)?.readLines()?.indexOfFirst {
val line = properties?.readLines()?.indexOfFirst {
it.startsWith(DISTRIBUTION_URL_PROPERTY)
} ?: -1
val position = properties?.let { FilePosition(it.toNioPath().toFile(), line, 0) }
val position = properties?.let { FilePosition(it.toFile(), line, 0) }
console.addWarning(SyncBundle.message("maven.sync.wrapper.http.title"),
SyncBundle.message("maven.sync.wrapper.http.description"),
position)
@@ -254,6 +255,7 @@ internal class MavenWrapperSupport {
MavenWrapperMapping.getInstance().myState.mapping[createDistributionKey(project, urlString)] = path.toAbsolutePath().toString()
}
@JvmStatic
fun getCurrentDistribution(project: Project, urlString: String): MavenDistribution? {
val mapping = MavenWrapperMapping.getInstance()
@@ -272,7 +274,7 @@ internal class MavenWrapperSupport {
}
@JvmStatic
fun getWrapperProperties(baseDir: VirtualFile?) =
baseDir?.findChild(".mvn")?.findChild("wrapper")?.findChild("maven-wrapper.properties")
fun getWrapperProperties(baseDir: Path?): Path? =
baseDir?.resolve(".mvn")?.resolve("wrapper")?.resolve("maven-wrapper.properties")
}
}
@@ -111,7 +111,6 @@ import java.util.stream.Stream
import java.util.zip.CRC32
import javax.xml.parsers.ParserConfigurationException
import javax.xml.parsers.SAXParserFactory
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
object MavenUtil {
@@ -1985,12 +1984,14 @@ object MavenUtil {
fun isMaven410(xmlns: String?, schemaLocation: String?): Boolean {
if (xmlns == null || schemaLocation == null) return false
val schemaLocations = schemaLocation.split(' ')
return (xmlns == MAVEN_4_XLMNS || xmlns == MAVEN_4_XLMNS_HTTPS)
return (xmlns == MAVEN_4_XMLNS || xmlns == MAVEN_4_XMLNS_HTTPS)
&& schemaLocations.all {
it == MAVEN_4_XLMNS ||
it == MAVEN_4_XLMNS_HTTPS ||
it == MAVEN_4_XMLNS ||
it == MAVEN_4_XMLNS_HTTPS ||
it == MAVEN_4_XSD ||
it == MAVEN_4_XSD_HTTPS
it == MAVEN_4_XSD_HTTPS ||
it == MAVEN_4_XSD_UNDERSCORE ||
it == MAVEN_4_XSD_HTTPS_UNDERSCORE
}
}
@@ -18,7 +18,6 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.util.text.StringUtil
@@ -158,7 +157,7 @@ class MavenProjectAsyncBuilder {
tree.addManagedFilesWithProfiles(files, MavenExplicitProfiles.NONE)
generalSettings.updateFromMavenConfig(files)
updateMavenSettingsFromEnvironment(project, generalSettings, importingSettings)
updateMavenSettingsFromEnvironment(project, generalSettings, importingSettings, rootDirectory)
MavenSettingsCache.getInstance(project).reloadAsync()
val manager = MavenProjectsManager.getInstance(project)
@@ -210,6 +209,7 @@ class MavenProjectAsyncBuilder {
project: Project,
generalSettings: MavenGeneralSettings,
importingSettings: MavenImportingSettings,
rootDirectory: Path,
) {
val settings = MavenWorkspaceSettingsComponent.getInstance(project).settings
settings.generalSettings = generalSettings
@@ -218,7 +218,7 @@ class MavenProjectAsyncBuilder {
if (!settingsFile.isNullOrBlank()) {
settings.generalSettings.setUserSettingsFile(settingsFile.trim { it <= ' ' })
}
val distributionUrl = getWrapperDistributionUrl(project.guessProjectDir())
val distributionUrl = getWrapperDistributionUrl(rootDirectory)
if (distributionUrl != null) {
settings.generalSettings.mavenHomeType = MavenWrapper
}
@@ -58,7 +58,8 @@
<extensionPoint name="mavenAdditionalHighlighter" interface="org.jetbrains.idea.maven.dom.MavenAdditionalHightligher" dynamic="true"/>
<extensionPoint name="mavenRemoteConnectionCreator" interface="org.jetbrains.idea.maven.execution.MavenRemoteConnectionCreator" dynamic="true"/>
<extensionPoint name="mavenRemoteConnectionCreator" interface="org.jetbrains.idea.maven.execution.MavenRemoteConnectionCreator"
dynamic="true"/>
</extensionPoints>
<dependencies>
@@ -227,6 +228,14 @@
key="inspection.redundant.groupId.name"
groupKey="inspection.group" enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenRedundantGroupIdInspection"/>
<localInspection language="XML" shortName="MavenRedundantVersion" bundle="messages.MavenDomBundle"
key="inspection.redundant.version.name"
groupKey="inspection.group" enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenRedundantVersionInspection"/>
<localInspection language="XML" shortName="MavenNoRootDefined" bundle="messages.MavenDomBundle"
key="inspection.no.root.defined.name"
groupKey="inspection.group" enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenNoRootDefinedInspection"/>
<localInspection language="XML" shortName="MavenPropertyInParent" bundle="messages.MavenDomBundle"
key="inspection.property.in.parent.name"
groupKey="inspection.group" enabledByDefault="true" level="WARNING"
@@ -247,6 +256,26 @@
groupKey="inspection.group" enabledByDefault="true" level="ERROR"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenNewModelVersionInOldSchemaInspection"/>
<localInspection language="XML" shortName="MavenNewModelVersionInOldMaven" bundle="messages.MavenDomBundle"
key="inspection.new.model.version.with.old.maven"
groupKey="inspection.group" enabledByDefault="true" level="ERROR"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenNewModelVersionInOldMavenInspection"/>
<localInspection language="XML" shortName="MavenBomPackagingInOldSchema" bundle="messages.MavenDomBundle"
key="inspection.new.packaging.in.old.model.name"
groupKey="inspection.group" enabledByDefault="true" level="ERROR"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenBomPackagingInOldSchema"/>
<localInspection language="XML" shortName="Maven4RedundantParentCoordinates" bundle="messages.MavenDomBundle"
key="inspection.redundant.parent.coordinates.maven.4"
groupKey="inspection.group" enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.idea.maven.dom.inspections.Maven4RedundantParentCoordinatesInspection"/>
<localInspection language="XML" shortName="MavenModulesInMaven4" bundle="messages.MavenDomBundle"
key="inspection.modules.tag.in.maven.4"
groupKey="inspection.group" enabledByDefault="true" level="WARNING"
implementationClass="org.jetbrains.idea.maven.dom.inspections.MavenModulesInMaven4Inspection"/>
<xml.xmlExtension implementation="org.jetbrains.idea.maven.dom.MavenXmlExtension"/>
<dom.extender domClass="org.jetbrains.idea.maven.dom.model.MavenDomConfiguration"
extenderClass="org.jetbrains.idea.maven.dom.MavenPluginConfigurationDomExtender"/>
@@ -316,6 +345,12 @@
<codeInsight.unresolvedReferenceQuickFixProvider
implementation="org.jetbrains.idea.maven.dom.intentions.AddMavenDependencyQuickFixProvider"/>
<codeInsight.declarativeInlayProvider language="XML"
implementationClass="org.jetbrains.idea.maven.dom.inlay.MavenModelVersionInlayHintProvider"
providerId="maven.pom.model.version.hints" group="OTHER_GROUP" isEnabledByDefault="true"
bundle="messages.MavenDomBundle" nameKey="maven.version.inlay.hint.name"
descriptionKey="maven.version.inlay.hint.description"/>
<gotoFileContributor implementation="org.jetbrains.idea.maven.utils.MavenGotoFileContributor"/>
<gotoFileContributor implementation="org.jetbrains.idea.maven.utils.MavenGotoSettingsFileContributor"/>
<gotoFileContributor implementation="org.jetbrains.idea.maven.vfs.MavenGotoPropertyFileContributor"/>
@@ -352,6 +387,10 @@
implementation="org.jetbrains.idea.maven.statistics.MavenLibraryCoordinatesWhitelistValidationRule"/>
<editorFactoryListener implementation="org.jetbrains.idea.maven.editor.MavenModelVersionEditorFactoryListener"/>
<registryKey key="maven.sync.model.editing" defaultValue="true"
description="Synchronize editing model and xmlns in pom.xml"/>
<registryKey key="maven.collect.local.stat" defaultValue="false"
description="Collect local maven statistics. Used in Internal mode. Does not affect FUS"/>
@@ -1000,5 +1039,10 @@
<listener class="org.jetbrains.idea.maven.plugins.compatibility.PluginCompatibilityConfiguratorListener"
topic="org.jetbrains.idea.maven.project.MavenSyncListener"
activeInTestMode="false" activeInHeadlessMode="false"/>
<listener class="org.jetbrains.idea.maven.editor.MavenModelVersionSynchronizerService$MyCommandListener"
topic="com.intellij.openapi.command.CommandListener"/>
<listener class="org.jetbrains.idea.maven.editor.MavenModelVersionSynchronizerService$MyDynamicPluginListener"
topic="com.intellij.ide.plugins.DynamicPluginListener"/>
</applicationListeners>
</idea-plugin>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>/*<# Compatible with Maven 4 #>*/
<groupId>group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
@@ -0,0 +1,5 @@
<html>
<body>
Reports redundant parent coordinates for Maven 4 projects
</body>
</html>
@@ -0,0 +1,5 @@
<html>
<body>
Ensures that the new Maven packaging does not appear in the legacy model version
</body>
</html>
@@ -0,0 +1,5 @@
<html>
<body>
Reports the presence of the &lt;modules&gt; tag in the Maven 4.1.0 model version
</body>
</html>
@@ -0,0 +1,5 @@
<html>
<body>
Reporst about old maven version in projects with model 4.1.0
</body>
</html>
@@ -0,0 +1,5 @@
<html>
<body>
Reports the absence of the .mvn directory/root tag, or when .mvn is located above the managed root project
</body>
</html>
@@ -0,0 +1,5 @@
<html>
<body>
Reports an unnecessary &lt;version&gt; definition, as it is already specified in the parent pom.xml
</body>
</html>
@@ -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"
}
]
}
@@ -36,18 +36,30 @@ inspection.duplicate.dependencies.name=Duplicate Dependencies
inspection.duplicate.plugin.declaration=Duplicate plugin declaration
MavenDuplicateDependenciesInspection.has.duplicates=<html><body>Dependency is duplicated in file(s)#treeend: {0} </body></html>
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 <groupId>
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 <modules> tag is deprecated; use <subprojects> instead
inspection.modules.tag.in.maven.4.name=Change to <subprojects>
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
@@ -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
@@ -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("""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>
""".trimIndent())
))
}
fun testIsProjectFileWithModel410() {
assertTrue(MavenDomUtil.isProjectFileWithModel410(
createXmlFile("""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>
""".trimIndent())
))
}
fun testIsProjectFileWithModel410Incomplete() {
assertFalse(MavenDomUtil.isProjectFileWithModel410(
createXmlFile("""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>
""".trimIndent())
))
}
fun testIsProjectFileWithModel410Incomplete2() {
assertFalse(MavenDomUtil.isProjectFileWithModel410(
createXmlFile("""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>
""".trimIndent())
))
}
fun testIsProjectFileWithModel410NoModel() {
assertTrue(MavenDomUtil.isProjectFileWithModel410(
createXmlFile("""
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>
""".trimIndent())
))
}
fun createXmlFile(text: String) : PsiFile {
return PsiFileFactory.getInstance(project).createFileFromText("pom.xml", XmlFileType.INSTANCE, text);
}
}
@@ -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(
"""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
""".trimIndent())
val provider = MavenModelDocumentationProvider()
val psi = findPsiFile(projectPom) as XmlFile
val doc = readAction {
provider.generateDoc(psi.rootTag!!.findSubTags("modelVersion").single(), null)
}
assertEquals("Model property<br>project.modelVersion: <b>$modelVersion</b><br>Maven version: ${getActualVersion(myMavenVersion!!)}", doc)
}
}
@@ -139,31 +139,6 @@ class MavenModelValidationTest : MavenDomWithIndicesTestCase() {
checkHighlighting()
}
@Test
fun testUnknownModelVersionMaven4() = runBlocking {
assumeMaven4()
fixture.saveText(projectPom,
"""
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion><error descr="Unsupported model version. Only versions [4.0.0, 4.1.0] are supported.">666</error></modelVersion>
<artifactId>foo</artifactId>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testModelVersion41isUnsupportedInMaven3() = runBlocking {
assumeMaven3()
fixture.saveText(projectPom,
"""
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion><error descr="Unsupported model version. Only versions [4.0.0] are supported.">4.1.0</error></modelVersion>
<artifactId>foo</artifactId>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testModelVersion41isSupportedInMaven4() = runBlocking {
@@ -35,7 +35,7 @@ class MavenPackagingCompletionTest : MavenDomTestCase() {
<packaging><caret></packaging>
""".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
@@ -1207,6 +1207,165 @@ class MavenPropertyCompletionAndResolutionTest : MavenDomTestCase() {
assertCompletionVariantsDoNotInclude(projectPom, "project.groupId")
}
@Test
fun testCompletingMaven4Specific() = runBlocking {
assumeModel_4_1_0("applicable for maven4")
updateProjectPom("""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
<name>${'$'}{project.<caret></name>
""".trimIndent())
val variants = getCompletionVariants(projectPom)
assertContain(variants, "project.rootDirectory")
updateProjectPom("""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
<name>${'$'}{session.<caret></name>
""".trimIndent())
val sessionVariants = getCompletionVariants(projectPom)
assertContain(sessionVariants, "session.rootDirectory", "session.topDirectory")
}
@Test
fun testResolveMaven4SpecificRootDir() = runBlocking {
assumeModel_4_1_0("applicable for maven4")
updateProjectPom("""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
<properties>
<myProp>${'$'}{<caret>project.rootDirectory}</myProp>
</properties>
""".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",
"""
<parent>
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
""".trimIndent())
updateProjectPom("""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
</modules>
""".trimIndent())
updateAllProjects()
val m1 = updateModulePom("m1",
"""
<parent>
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
<properties>
<myDir>${'$'}{<caret>project.rootDirectory}</myDir>
</properties>
""".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",
"""
<parent>
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
""".trimIndent())
updateProjectPom("""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
</modules>
""".trimIndent())
updateAllProjects()
val m1 = updateModulePom("m1",
"""
<parent>
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
<properties>
<myDir>${'$'}{<caret>session.rootDirectory}</myDir>
</properties>
""".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",
"""
<parent>
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
""".trimIndent())
updateProjectPom("""
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
</modules>
""".trimIndent())
updateAllProjects()
val m1 = updateModulePom("m1",
"""
<parent>
<groupId>test</groupId>
<artifactId>project</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
<properties>
<myDir>${'$'}{<caret>session.topDirectory}</myDir>
</properties>
""".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()
@@ -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("""<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0<caret>.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""", "\b1", """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""")
}
fun testTypingInModeWithUnderscoresSchemaLocation() {
doTest("""<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0<caret>.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""", "\b1", """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/maven-v4_1_0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""")
}
fun testTypingInUnderscoresSchemaLocation() {
doTest("""<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0<caret>_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""", "\b1", """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/maven-v4_1_0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""")
}
fun testTypingInXmlns() {
doTest("""<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0<caret>.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""", "\b1", """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""")
}
fun testTypingInSchemaLocationPom() {
doTest("""<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0<caret>.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""", "\b1", """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""")
}
fun testTypingInSchemaLocationXsd() {
doTest("""<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0<caret>.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""", "\b1", """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</project>""")
}
fun doTest(content: String, toType: String, expected: String) {
val xmlFileType = FileTypeManager.getInstance().getFileTypeByExtension("xml")
myFixture.configureByText(xmlFileType, content)
myFixture.type(toType)
myFixture.checkResult(expected)
}
}
@@ -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", """
<parent>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
</parent>
<artifactId>m1</artifactId>
""")
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
</modules>
""")
checkHighlighting(moduleFile)
}
@Test
fun testFireHighlightInMaven4() = runBlocking {
assumeMaven4()
val moduleFile = createModulePom("m1", """
<parent>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><groupId>test</groupId></warning>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><artifactId>test</artifactId></warning>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><version>1</version></warning>
</parent>
<artifactId>m1</artifactId>
""")
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
</modules>
""")
checkHighlighting(moduleFile)
}
@Test
fun testDoQuickFixForParent() = runBlocking {
assumeMaven4()
val moduleFile = createModulePom("m1", """
<parent>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><groupId>test</groupId></warning>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><artifactId>test</artifactId></warning>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><caret><version>1</version></warning>
</parent>
<artifactId>m1</artifactId>
""")
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
</modules>
""")
checkHighlighting(moduleFile)
val intention = fixture.availableIntentions.singleOrNull{it.text == "Remove unnecessary tags"}
assertNotNull("Cannot find intention", intention)
fixture.launchAction(intention!!)
fixture.checkResult(createPomXml("""
<parent/>
<artifactId>m1</artifactId>
"""), true)
}
@Test
fun testDoQuickFixForParentWithRelativePath() = runBlocking {
assumeMaven4()
val module1File = createModulePom("m1", """
<groupId>test</groupId>
<version>1</version>
<packaging>pom</packaging>
<artifactId>m1</artifactId>
""")
val module2File = createModulePom("m2", """
<parent>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><groupId>test</groupId></warning>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><artifactId>m1</artifactId></warning>
<warning descr="The parent coordinates are redundant and not required in Maven 4"><caret><version>1</version></warning>
<relativePath>../m1/pom.xml</relativePath>
</parent>
<artifactId>m2</artifactId>
""")
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1</version>
<packaging>pom</packaging>
<modules>
<module>m1</module>
<module>m2</module>
</modules>
""")
checkHighlighting(module2File)
val intention = fixture.availableIntentions.singleOrNull{it.text == "Remove unnecessary tags"}
assertNotNull("Cannot find intention", intention)
fixture.launchAction(intention!!)
fixture.checkResult(createPomXml("""
<parent>
<relativePath>../m1/pom.xml</relativePath>
</parent>
<artifactId>m2</artifactId>
"""), true)
}
}
@@ -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("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
""".trimIndent())
}
}
@Test
fun testDoNotFireHighlightIn41() = runBlocking {
assumeModel_4_1_0("")
createProjectPom("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
<packaging>bom</packaging>
""".trimIndent())
checkHighlighting()
}
@Test
fun testFireHighlightIn40() = runBlocking {
assumeModel_4_0_0("")
setRawPomFile("""<?xml version="1.0"?>
<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><error descr="Model version 4.1.0 is required for packaging bom">4.0.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
<packaging><error descr="Model version 4.1.0 is required for packaging bom">bom</error></packaging>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testUpdateToModel41() = runBlocking {
assumeModel_4_0_0("")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion><error descr="Model version 4.1.0 is required for packaging bom">4.0.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
<packaging><error descr="Model version 4.1.0 is required for packaging bom">bo<caret>m</error></packaging> </project>
""".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("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
<packaging>bom</packaging> </project>""".trimIndent())
}
}
@@ -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", """
<groupId>test</groupId>
<artifactId>m1</artifactId>
<version>test</version>
""")
createModulePom("m2", """
<groupId>test</groupId>
<artifactId>m2</artifactId>
<version>test</version>
""")
createModulePom("m1/sub1", """
<groupId>test</groupId>
<artifactId>sub1</artifactId>
<version>test</version>
""")
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>test</version>
<modules>
<module>m1</module>
<module>m2</module>
<module>m1/sub1</module>
</modules>
""")
createProjectPom("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>test</version>
<warning descr="Tag <modules> is deprecated, should be replaced to <subprojects>"><modules>
<module>m1</module>
<module>m2</module>
<module>m1/sub1</module>
</modules><caret></warning>
""")
checkHighlighting()
val intention = fixture.availableIntentions.singleOrNull { it.text == "Replace to <subprojects>" }
assertNotNull("Cannot find intention", intention)
fixture.launchAction(intention!!)
fixture.checkResult("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>test</version>
<subprojects>
<subproject>m1</subproject>
<subproject>m2</subproject>
<subproject>m1/sub1</subproject>
</subprojects>
</project>""")
}
}
@@ -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("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
""".trimIndent())
}
}
@Test
fun testCheckHighlightingWrongModel() = runBlocking{
assumeModel_4_0_0("testing only for model 4.0.0")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion><error descr="Model version 4.1.0 is required for projects with 4.1.0 schema">4.1.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testCheckNotHighlightingModel4_0() = runBlocking{
assumeModel_4_0_0("testing only for model 4.0.0")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testCheckNotHighlightingModel4_1() = runBlocking{
assumeModel_4_1_0("testing only for model 4.1.0")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testCheckNotHighlightingModel4_1_underscore() = runBlocking{
assumeModel_4_1_0("testing only for model 4.1.0")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/maven-v4_1_0.xsd ">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testCheckQuickFix() = runBlocking{
assumeModel_4_0_0("testing only for model 4.0.0")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion><error descr="Model version 4.1.0 is required for projects with 4.1.0 schema"><caret>4.1.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".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("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
}
@Test
fun testCheckQuickFixAddsXMLSchemaInstance() = runBlocking{
assumeModel_4_0_0("testing only for model 4.0.0")
setRawPomFile("""<?xml version="1.0"?>
<project>
<modelVersion><error descr="Model version 4.1.0 is required for projects with 4.1.0 schema"><caret>4.1.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".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("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
}
@Test
fun testQuickFixLeavesHttpsIfWasDefined() = runBlocking{
assumeModel_4_0_0("testing only for model 4.0.0")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion><error descr="Model version 4.1.0 is required for projects with 4.1.0 schema"><caret>4.1.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".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("""<?xml version="1.0"?>
<project xmlns="https://maven.apache.org/POM/4.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
}
}
@@ -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("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>test</version>
""")
checkHighlighting()
}
@Test
fun testCheckHighlightingInOldMaven() = runBlocking {
assumeMaven3()
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>test</version>
""")
setRawPomFile("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd">
<modelVersion><error descr="Maven version 4+ is required for projects with 4.1.0 schema">4.1.0</error></modelVersion>
<groupId>my.group</groupId>
<artifactId>artifact</artifactId>
<version>1.0</version>
</project>
""".trimIndent())
checkHighlighting()
}
}
@@ -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("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
""".trimIndent())
createProjectSubDir(".mvn")
checkHighlighting()
}
@Test
fun testHighlightingWithoutMvn() = runBlocking {
importProjectAsync("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
""".trimIndent())
setRawPomFile("""<?xml version="1.0"?>
<warning descr="Multi-module directory should be defined for all maven projects"><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>$modelVersion</modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version></project></warning>
""".trimIndent())
checkHighlighting()
}
@Test
fun testHighlightingWithRoot() = runBlocking {
assumeModel_4_1_0("only for model 4.1.0")
importProjectAsync("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
""".trimIndent())
setRawPomFile("""
<?xml version="1.0"?>
<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"
root="true">
<modelVersion>$modelVersion</modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version></project>
""".trimIndent())
checkHighlighting()
}
@Test
fun testRootTagQuickFix() = runBlocking {
assumeModel_4_1_0("applicable for model version 4.1.0")
importProjectAsync("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
""".trimIndent())
val mvnDir = projectRoot.toNioPath().resolve(".mvn")
assertFalse("Directory should not exist!", mvnDir.isDirectory())
setRawPomFile("""<?xml version="1.0"?>
<warning descr="Multi-module directory should be defined for all maven projects"><<caret>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>$modelVersion</modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version></project></warning>
""".trimIndent())
checkHighlighting()
val intention = fixture.availableIntentions.singleOrNull{it.text.contains("Add root")}
assertNotNull("Cannot find intention", intention)
fixture.launchAction(intention!!)
fixture.checkResult("""<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 http://maven.apache.org/xsd/maven-4.1.0.xsd"
root="true">
<modelVersion>4.1.0</modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version></project>
""".trimIndent())
}
@Test
fun testMvnDirectoryQuickFix() = runBlocking {
importProjectAsync("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
""".trimIndent())
val mvnDir = projectRoot.toNioPath().resolve(".mvn")
assertFalse("Directory should not exist!", mvnDir.isDirectory())
setRawPomFile("""<?xml version="1.0"?>
<warning descr="Multi-module directory should be defined for all maven projects"><<caret>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>$modelVersion</modelVersion>
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version></project></warning>
""".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())
}
}
@@ -11,6 +11,14 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() {
super.setUp()
fixture.enableInspections(MavenRedundantGroupIdInspection::class.java)
runBlocking {
importProjectAsync("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
""".trimIndent())
}
}
@Test
@@ -31,11 +39,11 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() {
<artifactId>childA</artifactId>
<version>1.0</version>
<parent>
<groupId>my.group</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<parent>
<groupId><error descr="Project 'my.group:parent:1.0' not found">my.group</error></groupId>
<artifactId><error descr="Project 'my.group:parent:1.0' not found">parent</error></artifactId>
<version><error descr="Project 'my.group:parent:1.0' not found">1.0</error></version>
</parent>
""".trimIndent())
checkHighlighting()
@@ -49,10 +57,10 @@ class MavenRedundantGroupIdTest : MavenDomTestCase() {
<version>1.0</version>
<parent>
<groupId>my.group</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<groupId><error descr="Project 'my.group:parent:1.0' not found">my.group</error></groupId>
<artifactId><error descr="Project 'my.group:parent:1.0' not found">parent</error></artifactId>
<version><error descr="Project 'my.group:parent:1.0' not found">1.0</error></version>
</parent>
""".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("<groupId>")) {
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)
@@ -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("""
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>1.0</version>
""".trimIndent())
}
}
@Test
fun testHighlighting1() = runBlocking {
createProjectPom("""
<groupId>my.group</groupId>
<artifactId>childA</artifactId>
<version>1.0</version>
""".trimIndent())
checkHighlighting()
}
@Test
fun testHighlighting2() = runBlocking {
createProjectPom("""
<groupId>childGroupId</groupId>
<artifactId>childA</artifactId>
<version>2.0</version>
<parent>
<groupId><error descr="Project 'my.group:parent:1.0' not found">my.group</error></groupId>
<artifactId><error descr="Project 'my.group:parent:1.0' not found">parent</error></artifactId>
<version><error descr="Project 'my.group:parent:1.0' not found">1.0</error></version>
</parent>
""".trimIndent())
checkHighlighting()
}
@Test
fun testHighlighting3() = runBlocking {
createProjectPom("""
<artifactId>childA</artifactId>
<warning descr="Definition of version is redundant, because it's inherited from the parent"><version>1.0</version></warning>
<parent>
<groupId><error descr="Project 'my.group:parent:1.0' not found">my.group</error></groupId>
<artifactId><error descr="Project 'my.group:parent:1.0' not found">parent</error></artifactId>
<version><error descr="Project 'my.group:parent:1.0' not found">1.0</error></version>
</parent>
""".trimIndent())
checkHighlighting()
}
@Test
fun testQuickFix() = runBlocking {
createProjectPom("""
<artifactId>childA</artifactId>
<groupId>mavenParent</groupId>
<version<caret>>1.0</version>
<parent>
<groupId>mavenParent</groupId>
<artifactId>mavenParent</artifactId>
<version>1.0</version>
</parent>
""".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("""
<artifactId>childA</artifactId>
<groupId>mavenParent</groupId>
<parent>
<groupId>mavenParent</groupId>
<artifactId>mavenParent</artifactId>
<version>1.0</version>
</parent>
""".trimIndent()))
}
}
@@ -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)
}
@@ -736,4 +736,5 @@ abstract class MavenDomTestCase : MavenMultiVersionImportingTestCase() {
}
}
@@ -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!!
}
@@ -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 = "<project>", suffix = "</project>") xml: @NonNls String?): @NonNls String {