diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts index e1a7df7b7b9..966344b8be1 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/build.gradle.kts @@ -3,6 +3,7 @@ dependencies { implementation(project(":azure-intellij-plugin-lib-java")) // runtimeOnly project(path: ":azure-intellij-plugin-lib", configuration: "instrumentedJar") implementation("com.microsoft.azure:azure-toolkit-ide-common-lib") + testImplementation("org.mockito:mockito-core:5.20.0") intellijPlatform { // Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins. bundledPlugin("com.intellij.java") diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java index d5b2a73a7dc..a8dfd1d880c 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/JavaUpgradeCheckStartupActivity.java @@ -20,6 +20,7 @@ import kotlin.Unit; import kotlin.coroutines.Continuation; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.idea.maven.project.MavenProjectsManager; import reactor.core.publisher.Mono; import javax.annotation.Nonnull; @@ -39,6 +40,16 @@ public class JavaUpgradeCheckStartupActivity implements ProjectActivity, DumbAwa @Override public Object execute(@Nonnull Project project, @Nonnull Continuation continuation) { + MavenProjectsManager.getInstance(project).addManagerListener( + new MavenProjectsManager.Listener() { + @Override + public void projectImportCompleted() { + performJavaUpgradeCheck(project, false); + } + }, + project + ); + // Wait for indexing to complete before running the check DumbService.getInstance(project).runWhenSmart(() -> { // Add a small delay after smart mode to ensure Maven/Gradle sync is done @@ -48,7 +59,7 @@ public Object execute(@Nonnull Project project, @Nonnull Continuation { /* Error during Java upgrade check startup */ @@ -63,7 +74,7 @@ public Object execute(@Nonnull Project project, @Nonnull Continuation dependencyIssues = cache.getDependencyIssues(); + final Properties mavenProperties = getMavenProperties(project, file); return new XmlElementVisitor() { @Override public void visitXmlTag(@NotNull XmlTag tag) { super.visitXmlTag(tag); - // Check for JDK version tags - if (jdkIssue != null) { - if (isJavaVersionProperty(tag) || isCompilerPluginVersionTag(tag)) { - registerProblem(holder, tag, jdkIssue); + if (isJavaVersionProperty(tag) || isCompilerPluginVersionTag(tag)) { + final JavaUpgradeIssue javaIssue = + JavaUpgradeProblemLocator.createJavaVersionIssue(tag, mavenProperties); + if (javaIssue != null) { + registerProblem(holder, tag, javaIssue); } } @@ -93,6 +96,16 @@ public void visitXmlTag(@NotNull XmlTag tag) { }; } + @NotNull + private Properties getMavenProperties(@NotNull Project project, @NotNull PsiFile file) { + if (file.getVirtualFile() == null) { + return new Properties(); + } + final MavenProjectsManager manager = MavenProjectsManager.getInstanceIfCreated(project); + final MavenProject mavenProject = + manager == null ? null : manager.findProject(file.getVirtualFile()); + return mavenProject == null ? new Properties() : mavenProject.getProperties(); + } private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag, @NotNull JavaUpgradeIssue issue) { log.info("Registering Java upgrade issue in inspection: {}", issue); holder.registerProblem( diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeProblemLocator.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeProblemLocator.java new file mode 100644 index 00000000000..90909fb76ec --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeProblemLocator.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.inspection; + +import com.intellij.psi.xml.XmlTag; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.JDK_DISPLAY_NAME; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.JDK_LEARN_MORE_URL; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.PACKAGE_ID_JDK; +import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.ISSUE_DISPLAY_NAME; + +final class JavaUpgradeProblemLocator { + + private JavaUpgradeProblemLocator() { + } + + static JavaUpgradeIssue createJavaVersionIssue(@NotNull XmlTag versionTag) { + return createJavaVersionIssue(versionTag, new Properties()); + } + + static JavaUpgradeIssue createJavaVersionIssue( + @NotNull XmlTag versionTag, + @NotNull Properties effectiveProperties + ) { + return createJavaVersionIssue( + resolveVersionText(versionTag, effectiveProperties, new HashSet<>())); + } + + static JavaUpgradeIssue createJavaVersionIssue(@Nullable String versionText) { + final Integer version = parseJavaVersion(versionText); + if (version == null || version < 8 || version >= MATURE_JAVA_LTS_VERSION) { + return null; + } + return JavaUpgradeIssue.builder() + .packageId(PACKAGE_ID_JDK) + .packageDisplayName(JDK_DISPLAY_NAME) + .upgradeReason(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) + .severity(JavaUpgradeIssue.Severity.WARNING) + .currentVersion(String.valueOf(version)) + .supportedVersion(">=" + MATURE_JAVA_LTS_VERSION) + .suggestedVersion(String.valueOf(MATURE_JAVA_LTS_VERSION)) + .message(String.format( + ISSUE_DISPLAY_NAME, + JDK_DISPLAY_NAME, + version, + JDK_DISPLAY_NAME, + MATURE_JAVA_LTS_VERSION + )) + .learnMoreUrl(JDK_LEARN_MORE_URL) + .build(); + } + + @Nullable + private static String resolveVersionText( + @NotNull XmlTag versionTag, + @NotNull Properties effectiveProperties, + @NotNull Set resolvingProperties + ) { + final String versionText = versionTag.getValue().getText().trim(); + if (!versionText.startsWith("${") || !versionText.endsWith("}")) { + return versionText; + } + + final String propertyName = versionText.substring(2, versionText.length() - 1); + if (!resolvingProperties.add(propertyName)) { + return null; + } + + XmlTag current = versionTag; + while (current != null) { + final XmlTag properties = "properties".equals(current.getName()) + ? current + : current.findFirstSubTag("properties"); + if (properties != null) { + final XmlTag property = properties.findFirstSubTag(propertyName); + if (property != null) { + return resolveVersionText(property, effectiveProperties, resolvingProperties); + } + } + current = current.getParentTag(); + } + return resolvePropertyValue( + effectiveProperties.getProperty(propertyName), + effectiveProperties, + resolvingProperties + ); + } + + @Nullable + private static String resolvePropertyValue( + @Nullable String value, + @NotNull Properties effectiveProperties, + @NotNull Set resolvingProperties + ) { + if (value == null) { + return null; + } + final String normalized = value.trim(); + if (!normalized.startsWith("${") || !normalized.endsWith("}")) { + return normalized; + } + final String propertyName = normalized.substring(2, normalized.length() - 1); + if (!resolvingProperties.add(propertyName)) { + return null; + } + return resolvePropertyValue( + effectiveProperties.getProperty(propertyName), + effectiveProperties, + resolvingProperties + ); + } + + @Nullable + private static Integer parseJavaVersion(@Nullable String versionText) { + if (versionText == null) { + return null; + } + final String normalized = versionText.trim(); + if (normalized.isEmpty() || normalized.startsWith("${")) { + return null; + } + final String majorVersion = normalized.startsWith("1.") + ? normalized.substring(2) + : normalized; + final int separator = majorVersion.indexOf('.'); + final String major = separator < 0 ? majorVersion : majorVersion.substring(0, separator); + try { + return Integer.parseInt(major); + } catch (NumberFormatException ignored) { + return null; + } + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java index d8499172a0b..e831cdf6143 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/service/JavaUpgradeIssuesDetectionService.java @@ -192,7 +192,7 @@ public String getEolDateForVersion(@Nonnull String version) { ) ); - private static final String JDK_LEARN_MORE_URL = + public static final String JDK_LEARN_MORE_URL = "https://learn.microsoft.com/azure/developer/java/fundamentals/java-support-on-azure"; private static JavaUpgradeIssuesDetectionService instance; diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java index e8a6c0bc8ae..6fdfc906752 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/Constants.java @@ -12,7 +12,7 @@ public class Constants { public static final String SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME = "Scan and Resolve CVEs with Copilot"; public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT = "Fix the vulnerable dependency %s by using #appmod-validate-cves-for-java"; public static final String FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME = "Fix the vulnerable dependency with Copilot"; - public static final String ISSUE_DISPLAY_NAME = "Your project uses %s %s. Consider upgrading to %s to the latest LTS version for better performance and support"; + public static final String ISSUE_DISPLAY_NAME = "Your project uses %s %s. Consider upgrading %s to %s, the latest LTS version, for better performance and support"; public static final String APPMOD_CVE_AGENT_NAME = "modernize-java-security"; public static final String APPMOD_UPGRADE_AGENT_NAME = "modernize-java-upgrade"; } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml index 29c2ad3de56..947419df381 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/resources/META-INF/azure-intellij-plugin-appmod.xml @@ -45,10 +45,10 @@ description="Fix this vulnerable dependency using GitHub Copilot"> - + diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/test/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveActionsTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/test/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveActionsTest.java new file mode 100644 index 00000000000..14892bce073 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/test/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveActionsTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +public class CveActionsTest { + + @Test + public void exposesFrameworkUpgradeAsTopLevelProblemsViewAction() { + final String description = + "Your project uses Spring Boot 2.0.1.RELEASE. " + + "Consider upgrading Spring Boot to 3.5, the latest LTS version, " + + "for better performance and support"; + + final UpgradeInProblemsViewAction.UpgradeDescription upgrade = + UpgradeInProblemsViewAction.parseUpgradeDescription(description); + + assertNotNull(upgrade); + assertEquals("Spring Boot", upgrade.packageDisplayName()); + assertEquals("2.0.1.RELEASE", upgrade.currentVersion()); + assertEquals("3.5", upgrade.suggestedVersion()); + assertEquals( + "Upgrade Spring Boot with Copilot", + UpgradeInProblemsViewAction.getActionName(upgrade) + ); + } + + @Test + public void doesNotExposeTopLevelUpgradeActionForCveProblems() { + assertNull(UpgradeInProblemsViewAction.parseUpgradeDescription( + "Security vulnerability CVE-2020-36518 detected in " + + "maven:com.fasterxml.jackson.core:jackson-databind:2.9.4 Upgrade required" + )); + } +} diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/test/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspectionTest.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/test/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspectionTest.java new file mode 100644 index 00000000000..c8bb985d589 --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/test/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspectionTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +package com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.inspection; + +import com.intellij.psi.xml.XmlTag; +import com.intellij.psi.xml.XmlTagValue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import org.junit.Test; + +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class JavaUpgradeIssuesInspectionTest { + + @Test + public void createsJavaIssueFromPomJavaVersion() { + final JavaUpgradeIssue issue = JavaUpgradeProblemLocator.createJavaVersionIssue("8"); + + assertEquals("8", issue.getCurrentVersion()); + assertEquals(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD, issue.getUpgradeReason()); + } + + @Test + public void acceptsLegacyJavaVersionSyntax() { + final JavaUpgradeIssue issue = JavaUpgradeProblemLocator.createJavaVersionIssue("1.8"); + + assertEquals("8", issue.getCurrentVersion()); + } + + @Test + public void ignoresSupportedOrPropertyReferenceVersions() { + assertNull(JavaUpgradeProblemLocator.createJavaVersionIssue("21")); + assertNull(JavaUpgradeProblemLocator.createJavaVersionIssue("${java.version}")); + } + + @Test + public void resolvesJavaVersionFromAnotherMavenProperty() { + final XmlTag properties = mock(XmlTag.class); + final XmlTag javaVersion = valueTag("${jdk.version}"); + final XmlTag jdkVersion = valueTag("8"); + when(properties.getName()).thenReturn("properties"); + when(properties.findFirstSubTag("jdk.version")).thenReturn(jdkVersion); + when(javaVersion.getParentTag()).thenReturn(properties); + + final JavaUpgradeIssue issue = + JavaUpgradeProblemLocator.createJavaVersionIssue(javaVersion); + + assertEquals("8", issue.getCurrentVersion()); + } + + @Test + public void resolvesJavaVersionFromEffectiveParentProperties() { + final XmlTag javaVersion = valueTag("${parent.java.version}"); + final Properties effectiveProperties = new Properties(); + effectiveProperties.setProperty("parent.java.version", "8"); + + final JavaUpgradeIssue issue = + JavaUpgradeProblemLocator.createJavaVersionIssue(javaVersion, effectiveProperties); + + assertEquals("8", issue.getCurrentVersion()); + } + + private static XmlTag valueTag(String value) { + final XmlTag tag = mock(XmlTag.class); + final XmlTagValue tagValue = mock(XmlTagValue.class); + when(tag.getValue()).thenReturn(tagValue); + when(tagValue.getText()).thenReturn(value); + return tag; + } +}