diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java index ca1fc05af1..064b4ab126 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyInProblemsViewAction.java @@ -92,7 +92,8 @@ public void update(@NotNull AnActionEvent e) { return; } e.getPresentation().setEnabledAndVisible(true); - // e.getPresentation().setText(SCAN_AND_RESOLVE_CVES_WITH_COPILOT_DISPLAY_NAME); + e.getPresentation().setText(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME); + if (!AppModPluginInstaller.isAppModPluginInstalled()) { e.getPresentation().setText(e.getPresentation().getText() + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java index 8f3114202b..d8e914a176 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/CveFixDependencyIntentionAction.java @@ -19,11 +19,14 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaVersionNotificationService; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.GradleBuildFileUtils; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.PomXmlUtils; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants.*; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants; + +import java.util.Map; /** * Intention action to fix CVE vulnerabilities in dependencies using GitHub Copilot. @@ -41,9 +44,9 @@ public class CveFixDependencyIntentionAction implements IntentionAction, HighPri @Override public @IntentionName @NotNull String getText() { if (!AppModPluginInstaller.isAppModPluginInstalled()) { - return FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; + return Constants.FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME + AppModPluginInstaller.TO_INSTALL_APP_MODE_PLUGIN; } - return FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME; + return Constants.FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_DISPLAY_NAME; } @Override @@ -61,36 +64,58 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file return false; } - // Only available for pom.xml files + // Only available for pom.xml or Gradle build files final String fileName = file.getName(); - if (!fileName.equals("pom.xml")) { + final boolean isPomFile = fileName.equals("pom.xml"); + final boolean isGradleFile = fileName.endsWith(".gradle") || fileName.endsWith(".gradle.kts"); + if (!isPomFile && !isGradleFile) { return false; } final int offset = editor.getCaretModel().getOffset(); final String documentText = editor.getDocument().getText(); - - // Try to extract dependency info - only show if cursor is within a block - final int dependencyStart = PomXmlUtils.findDependencyStart(documentText, offset); - final int dependencyEnd = PomXmlUtils.findDependencyEnd(documentText, offset); - - if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { - final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); - String cachedGroupId = PomXmlUtils.extractXmlValue(dependencyBlock, "groupId"); - String cachedArtifactId = PomXmlUtils.extractXmlValue(dependencyBlock, "artifactId"); - String cachedVersion = PomXmlUtils.extractXmlValue(dependencyBlock, "version"); - vulnerabilityInfo = VulnerabilityInfo.builder().groupId(cachedGroupId).artifactId(cachedArtifactId).version(cachedVersion).build(); - // Only show if we have valid dependency info (not for parent/plugin sections) - if(cachedGroupId != null && cachedArtifactId != null) { - //if the artifact is in the cached cve issues, show the intention - final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(cachedGroupId + ":" + cachedArtifactId); - return issue != null; + + if (isPomFile) { + // Try to extract dependency info - only show if cursor is within a block + final int dependencyStart = PomXmlUtils.findDependencyStart(documentText, offset); + final int dependencyEnd = PomXmlUtils.findDependencyEnd(documentText, offset); + + if (dependencyStart >= 0 && dependencyEnd > dependencyStart) { + final String dependencyBlock = documentText.substring(dependencyStart, dependencyEnd); + String cachedGroupId = PomXmlUtils.extractXmlValue(dependencyBlock, "groupId"); + String cachedArtifactId = PomXmlUtils.extractXmlValue(dependencyBlock, "artifactId"); + String cachedVersion = PomXmlUtils.extractXmlValue(dependencyBlock, "version"); + vulnerabilityInfo = VulnerabilityInfo.builder().groupId(cachedGroupId).artifactId(cachedArtifactId).version(cachedVersion).build(); + // Only show if we have valid dependency info (not for parent/plugin sections) + if (cachedGroupId != null && cachedArtifactId != null) { + //if the artifact is in the cached cve issues, show the intention + final var issue = JavaUpgradeIssuesCache.getInstance(project).findCveIssue(cachedGroupId + ":" + cachedArtifactId); + if (issue != null) { + return true; + } + return false; + } + } + } else if (isGradleFile) { + final GradleBuildFileUtils.DependencyCoordinate coordinate = + GradleBuildFileUtils.findDependencyAtOffset(documentText, offset); + if (coordinate.isValid()) { + vulnerabilityInfo = VulnerabilityInfo.builder() + .groupId(coordinate.groupId()) + .artifactId(coordinate.artifactId()) + .version(coordinate.version()) + .build(); + final var issue = JavaUpgradeIssuesCache.getInstance(project) + .findCveIssue(coordinate.getPackageId()); + if (issue != null) { + return true; + } + return false; } } } catch (Throwable e) { // Ignore and return false log.error("Error checking availability of CveFixDependencyIntentionAction", e); } - return false; } @@ -102,9 +127,9 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws } // Try to extract dependency information from the current context - final String prompt = buildPromptFromContext(editor, file); + final String prompt = buildPromptFromContext(); JavaVersionNotificationService.getInstance().openCopilotChatWithPrompt(project, prompt); - AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction"); + AppModUtils.logTelemetryEvent("openCveFixDependencyCopilotChatFromIntentionAction", Map.of("appmodPluginInstalled", String.valueOf(AppModPluginInstaller.isAppModPluginInstalled()))); } catch (Throwable e) { log.error("Failed to invoke CveFixDependencyIntentionAction: ", e); } @@ -113,12 +138,12 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws /** * Builds a prompt based on the current editor context. */ - private String buildPromptFromContext(@NotNull Editor editor, @NotNull PsiFile file) { + private String buildPromptFromContext() { if (vulnerabilityInfo == null) { log.error("Vulnerability info is null in buildPromptFromContext"); - return SCAN_AND_RESOLVE_CVES_PROMPT; + return Constants.SCAN_AND_RESOLVE_CVES_PROMPT; } - return String.format(FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, + return String.format(Constants.FIX_VULNERABLE_DEPENDENCY_WITH_COPILOT_PROMPT, vulnerabilityInfo.getDependencyCoordinate()); } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java index 4f56c35018..69c4efe575 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/action/JavaUpgradeContextMenuAction.java @@ -59,9 +59,6 @@ public void update(@NotNull AnActionEvent e) { if (!isAppModPluginInstalled()) { e.getPresentation().setText(e.getPresentation().getText() + TO_INSTALL_APP_MODE_PLUGIN); } - if (visible){ - AppModUtils.logTelemetryEvent("showJavaUpgradeContextMenuAction", Map.of("appmodPluginInstalled", String.valueOf(isAppModPluginInstalled()))); - } e.getPresentation().setEnabledAndVisible(visible); } catch (Throwable ex) { // In case of any error, hide the action diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java index d998ed6904..d18e1ea4a1 100644 --- a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/inspection/JavaUpgradeIssuesInspection.java @@ -9,6 +9,7 @@ import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.PsiFile; import com.intellij.psi.XmlElementVisitor; @@ -17,17 +18,19 @@ import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.JavaUpgradeQuickFix; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesCache; - -import static com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService.*; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.service.JavaUpgradeIssuesDetectionService; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.GradleBuildFileUtils; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.Constants; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; /** * Inspection that displays Java upgrade issues detected by JavaUpgradeDetectionService. - * Shows JDK version and framework version issues in pom.xml files with wavy underlines. + * Shows JDK version and framework version issues in pom.xml and Gradle build files with wavy underlines. * * Note: Issues are cached at project startup via JavaUpgradeIssueCache to avoid * repeated expensive scans during inspection runs. @@ -40,8 +43,12 @@ public class JavaUpgradeIssuesInspection extends LocalInspectionTool { public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { final PsiFile file = holder.getFile(); - // Only process pom.xml files - if (!(file instanceof XmlFile) || !file.getName().equals("pom.xml")) { + final boolean isPomFile = file instanceof XmlFile && file.getName().equals("pom.xml"); + final boolean isGradleBuildFile = GradleBuildFileUtils.isGradleBuildFile(file); + final boolean isVersionCatalogFile = GradleBuildFileUtils.isVersionCatalogFile(file); + + // Only process pom.xml, Gradle build files, or version catalog files + if (!isPomFile && !isGradleBuildFile && !isVersionCatalogFile) { return PsiElementVisitor.EMPTY_VISITOR; } @@ -54,41 +61,116 @@ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean is } // Get cached issues (computed once at project startup) + // Note: CVE issues are handled separately by CveFixDependencyIntentionAction final JavaUpgradeIssue jdkIssue = cache.getJdkIssue(); final List dependencyIssues = cache.getDependencyIssues(); - return new XmlElementVisitor() { - @Override - public void visitXmlTag(@NotNull XmlTag tag) { - super.visitXmlTag(tag); + if (isPomFile) { + 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); + // Check for JDK version tags + if (jdkIssue != null) { + if (isJavaVersionProperty(tag) || isCompilerPluginVersionTag(tag)) { + registerProblem(holder, tag, jdkIssue); + } } - } - // Check for dependency/parent version tags and register all matching issues - if ("version".equals(tag.getName())) { - XmlTag parentElement = tag.getParentTag(); - if (parentElement != null) { - String parentTagName = parentElement.getName(); - if ("dependency".equals(parentTagName) || "parent".equals(parentTagName)) { - XmlTag groupIdTag = parentElement.findFirstSubTag("groupId"); - XmlTag artifactIdTag = parentElement.findFirstSubTag("artifactId"); - if (groupIdTag != null && artifactIdTag != null) { - String packageId = groupIdTag.getValue().getText() + ":" + artifactIdTag.getValue().getText(); - // Register all issues that match this package - for (JavaUpgradeIssue issue : dependencyIssues) { - if (matchesPackageId(packageId, issue.getPackageId())) { - registerProblem(holder, tag, issue); + // Check for dependency/parent version tags and register all matching issues + // Note: CVE issues are handled separately by CveFixDependencyIntentionAction + if ("version".equals(tag.getName())) { + XmlTag parentElement = tag.getParentTag(); + if (parentElement != null) { + String parentTagName = parentElement.getName(); + if ("dependency".equals(parentTagName) || "parent".equals(parentTagName)) { + XmlTag groupIdTag = parentElement.findFirstSubTag("groupId"); + XmlTag artifactIdTag = parentElement.findFirstSubTag("artifactId"); + if (groupIdTag != null && artifactIdTag != null) { + String packageId = groupIdTag.getValue().getText() + ":" + artifactIdTag.getValue().getText(); + // Register dependency upgrade issues + for (JavaUpgradeIssue issue : dependencyIssues) { + if (matchesPackageId(packageId, issue.getPackageId())) { + registerProblem(holder, tag, issue); + } } } } } } } + }; + } + + // Handle version catalog files (libs.versions.toml) + if (isVersionCatalogFile) { + return new PsiElementVisitor() { + @Override + public void visitFile(@NotNull PsiFile file) { + super.visitFile(file); + + final String text = file.getText(); + + for (GradleBuildFileUtils.GradleVersionLocation location : GradleBuildFileUtils.findSpringBootVersionsInCatalog(text)) { + final String packageId = JavaUpgradeIssuesDetectionService.GROUP_ID_SPRING_BOOT + ":catalog"; + for (JavaUpgradeIssue issue : dependencyIssues) { + if (matchesPackageId(packageId, issue.getPackageId())) { + registerProblem(holder, file, location.startOffset(), issue); + } + } + } + } + }; + } + + return new PsiElementVisitor() { + @Override + public void visitFile(@NotNull PsiFile file) { + super.visitFile(file); + + final String text = file.getText(); + + // Track registered offsets to avoid duplicate problems + final java.util.Set registeredOffsets = new java.util.HashSet<>(); + + GradleBuildFileUtils.findJavaVersionLocations(text) + .forEach(location -> { + final JavaUpgradeIssue gradleJdkIssue = buildJdkIssueForVersion(location.version()); + if (gradleJdkIssue != null) { + if (registeredOffsets.add(location.startOffset())) { + registerProblem(holder, file, location.startOffset(), gradleJdkIssue); + } + } else if (jdkIssue != null) { + if (registeredOffsets.add(location.startOffset())) { + registerProblem(holder, file, location.startOffset(), jdkIssue); + } + } + }); + + // Note: CVE issues are handled separately by CveFixDependencyIntentionAction + for (GradleBuildFileUtils.GradleDependencyLocation location : GradleBuildFileUtils.findDependencyLocations(text)) { + final String packageId = location.groupId() + ":" + location.artifactId(); + // Register dependency upgrade issues + for (JavaUpgradeIssue issue : dependencyIssues) { + if (matchesPackageId(packageId, issue.getPackageId())) { + if (registeredOffsets.add(location.startOffset())) { + registerProblem(holder, file, location.startOffset(), issue); + } + } + } + } + + for (GradleBuildFileUtils.GradleVersionLocation location : GradleBuildFileUtils.findSpringBootPluginVersions(text)) { + final String packageId = JavaUpgradeIssuesDetectionService.GROUP_ID_SPRING_BOOT + ":plugin"; + for (JavaUpgradeIssue issue : dependencyIssues) { + if (matchesPackageId(packageId, issue.getPackageId())) { + if (registeredOffsets.add(location.startOffset())) { + registerProblem(holder, file, location.startOffset(), issue); + } + } + } + } } }; } @@ -103,6 +185,60 @@ private void registerProblem(@NotNull ProblemsHolder holder, @NotNull XmlTag tag ); } + private void registerProblem(@NotNull ProblemsHolder holder, @NotNull PsiFile file, int offset, @NotNull JavaUpgradeIssue issue) { + final PsiElement element = file.findElementAt(Math.max(0, offset)); + final PsiElement target = element != null ? element : file; + log.info("Registering Java upgrade issue in inspection: {}", issue); + holder.registerProblem( + target, + issue.getMessage(), + ProblemHighlightType.WEAK_WARNING, + new JavaUpgradeQuickFix(issue) + ); + } + + @Nullable + private JavaUpgradeIssue buildJdkIssueForVersion(@NotNull String versionText) { + final Integer version = parseJavaVersion(versionText); + if (version == null) { + return null; + } + if (version < 8) { + return null; + } + if (version >= JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION) { + return null; + } + + return JavaUpgradeIssue.builder() + .packageId(JavaUpgradeIssuesDetectionService.PACKAGE_ID_JDK) + .packageDisplayName(JavaUpgradeIssuesDetectionService.JDK_DISPLAY_NAME) + .upgradeReason(JavaUpgradeIssue.UpgradeReason.JRE_TOO_OLD) + .severity(JavaUpgradeIssue.Severity.WARNING) + .currentVersion(String.valueOf(version)) + .supportedVersion(">=" + JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION) + .suggestedVersion(String.valueOf(JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION)) + .message(String.format(Constants.ISSUE_DISPLAY_NAME, + JavaUpgradeIssuesDetectionService.JDK_DISPLAY_NAME, + version, + JavaUpgradeIssuesDetectionService.JDK_DISPLAY_NAME, + JavaUpgradeIssuesDetectionService.MATURE_JAVA_LTS_VERSION)) + .build(); + } + + @Nullable + @SuppressWarnings("UnnecessaryTemporaryOnConversionFromString") + private Integer parseJavaVersion(@NotNull String versionText) { + try { + if (versionText.startsWith("1.")) { + return Integer.parseInt(versionText.substring(2)); + } + return Integer.parseInt(versionText); + } catch (NumberFormatException ignored) { + return null; + } + } + /** * Checks if the packageId matches the issue's packageId pattern. * Supports wildcard patterns like "org.springframework.boot:*" to match any artifact in a group. @@ -147,7 +283,8 @@ private boolean isCompilerPluginVersionTag(@NotNull XmlTag tag) { while (current != null) { if ("plugin".equals(current.getName())) { XmlTag artifactIdTag = current.findFirstSubTag("artifactId"); - if (artifactIdTag != null && ARTIFACT_ID_MAVEN_COMPILER_PLUGIN.equals(artifactIdTag.getValue().getText())) { + if (artifactIdTag != null && JavaUpgradeIssuesDetectionService.ARTIFACT_ID_MAVEN_COMPILER_PLUGIN + .equals(artifactIdTag.getValue().getText())) { return true; } } 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 d8499172a0..15b1d8f146 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 @@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project; import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.dao.JavaUpgradeIssue; +import com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.utils.GradleBuildFileUtils; import com.microsoft.azure.toolkit.intellij.appmod.utils.AppModUtils; import com.microsoft.azure.toolkit.intellij.common.utils.JdkUtils; import com.microsoft.intellij.util.GradleUtils; @@ -18,13 +19,14 @@ import org.jetbrains.idea.maven.model.MavenArtifactNode; import org.jetbrains.idea.maven.project.MavenProject; import org.jetbrains.idea.maven.project.MavenProjectsManager; -import org.jetbrains.plugins.gradle.model.ExternalDependency; import org.jetbrains.plugins.gradle.model.ExternalProject; -import org.jetbrains.plugins.gradle.model.ExternalSourceSet; -import org.jetbrains.plugins.gradle.model.UnresolvedExternalDependency; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.util.*; @@ -363,15 +365,7 @@ public List getCVEIssues(@Nonnull Project project) { log.info("Checking Gradle project dependencies for CVE issues"); final List gradleProjects = GradleUtils.listGradleProjects(project); for (ExternalProject gradleProject : gradleProjects) { - final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); - if (main != null) { - main.getDependencies().stream() - .filter(dep -> !(dep instanceof UnresolvedExternalDependency)) - .filter(dep -> StringUtils.isNotBlank(dep.getVersion())) - .forEach(dep -> coordinateSet.add( - dep.getGroup() + ":" + dep.getName() + ":" + dep.getVersion() - )); - } + collectDirectGradleDependencies(gradleProject, coordinateSet); } } @@ -627,24 +621,7 @@ private MavenArtifact findDirectDependency(@Nonnull MavenProject mavenProject, private JavaUpgradeIssue checkGradleDependency(@Nonnull ExternalProject gradleProject, @Nonnull DependencyCheckItem checkItem, @Nonnull Set checkedPackages) { - final ExternalSourceSet main = gradleProject.getSourceSets().get("main"); - if (main == null) { - return null; - } - - // Find direct dependency - final ExternalDependency dependency = main.getDependencies().stream() - .filter(dep -> StringUtils.equalsIgnoreCase(checkItem.groupId, dep.getGroup()) && - ("*".equals(checkItem.artifactId) || StringUtils.equalsIgnoreCase(checkItem.artifactId, dep.getName()))) - .filter(dep -> !(dep instanceof UnresolvedExternalDependency)) - .findFirst() - .orElse(null); - - if (dependency == null) { - return null; - } - - final String version = dependency.getVersion(); + final String version = findDirectGradleDependencyVersion(gradleProject, checkItem); if (version == null || StringUtils.isBlank(version)) { return null; @@ -670,4 +647,128 @@ private JavaUpgradeIssue checkGradleDependency(@Nonnull ExternalProject gradlePr return null; } + + /** + * Gets all direct Gradle dependency locations from the build files of a Gradle project. + * This is the shared logic used by both dependency issue checking and CVE checking. + * + * @param gradleProject The Gradle project to scan + * @return List of dependency locations found in the build files + */ + @Nonnull + private List getDirectGradleDependencyLocations( + @Nonnull ExternalProject gradleProject) { + final List allLocations = new ArrayList<>(); + final Path projectDir = gradleProject.getProjectDir().toPath(); + final List buildFiles = List.of( + projectDir.resolve("build.gradle"), + projectDir.resolve("build.gradle.kts") + ); + + for (Path buildFile : buildFiles) { + if (!Files.exists(buildFile)) { + continue; + } + + final String text; + try { + text = Files.readString(buildFile, StandardCharsets.UTF_8); + } catch (IOException e) { + log.warn("Failed to read Gradle build file: {}", buildFile, e); + continue; + } + + allLocations.addAll(GradleBuildFileUtils.findDependencyLocations(text)); + } + + return allLocations; + } + + /** + * Gets the Spring Boot plugin version from a Gradle project's build files. + * Checks multiple sources: + * 1. Build files (build.gradle, build.gradle.kts) + * 2. Version catalog (gradle/libs.versions.toml) + * + * @param gradleProject The Gradle project to scan + * @return The Spring Boot plugin version, or null if not found + */ + @Nullable + private String getSpringBootPluginVersion(@Nonnull ExternalProject gradleProject) { + final Path projectDir = gradleProject.getProjectDir().toPath(); + + // Check build files first + final List buildFiles = List.of( + projectDir.resolve("build.gradle"), + projectDir.resolve("build.gradle.kts") + ); + + for (Path buildFile : buildFiles) { + if (!Files.exists(buildFile)) { + continue; + } + + final String text; + try { + text = Files.readString(buildFile, StandardCharsets.UTF_8); + } catch (IOException e) { + log.warn("Failed to read Gradle build file: {}", buildFile, e); + continue; + } + + final List pluginVersions = + GradleBuildFileUtils.findSpringBootPluginVersions(text); + if (!pluginVersions.isEmpty()) { + return pluginVersions.get(0).version(); + } + } + + // Check version catalog (gradle/libs.versions.toml) + final Path versionCatalog = projectDir.resolve("gradle").resolve("libs.versions.toml"); + if (Files.exists(versionCatalog)) { + try { + final String tomlContent = Files.readString(versionCatalog, StandardCharsets.UTF_8); + final List catalogVersions = + GradleBuildFileUtils.findSpringBootVersionsInCatalog(tomlContent); + if (!catalogVersions.isEmpty()) { + return catalogVersions.get(0).version(); + } + } catch (IOException e) { + log.warn("Failed to read version catalog: {}", versionCatalog, e); + } + } + + return null; + } + + @Nullable + private String findDirectGradleDependencyVersion(@Nonnull ExternalProject gradleProject, + @Nonnull DependencyCheckItem checkItem) { + final List locations = + getDirectGradleDependencyLocations(gradleProject); + + for (GradleBuildFileUtils.GradleDependencyLocation location : locations) { + final boolean groupMatches = StringUtils.equalsIgnoreCase(checkItem.groupId, location.groupId()); + final boolean artifactMatches = "*".equals(checkItem.artifactId) || + StringUtils.equalsIgnoreCase(checkItem.artifactId, location.artifactId()); + if (groupMatches && artifactMatches) { + return location.version(); + } + } + + // Check for Spring Boot plugin version + if (GROUP_ID_SPRING_BOOT.equals(checkItem.groupId)) { + return getSpringBootPluginVersion(gradleProject); + } + + return null; + } + + private void collectDirectGradleDependencies(@Nonnull ExternalProject gradleProject, @Nonnull Set coordinateSet) { + getDirectGradleDependencyLocations(gradleProject).stream() + .filter(location -> StringUtils.isNotBlank(location.version())) + .forEach(location -> coordinateSet.add( + location.groupId() + ":" + location.artifactId() + ":" + location.version() + )); + } } diff --git a/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/GradleBuildFileUtils.java b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/GradleBuildFileUtils.java new file mode 100644 index 0000000000..27e11d574a --- /dev/null +++ b/PluginsAndFeatures/azure-toolkit-for-intellij/azure-intellij-plugin-appmod/src/main/java/com/microsoft/azure/toolkit/intellij/appmod/javaupgrade/utils/GradleBuildFileUtils.java @@ -0,0 +1,494 @@ +/* + * 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.utils; + +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class for parsing and extracting information from Gradle build files. + */ +@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection", "UnnecessaryTemporaryOnConversionFromString"}) +public final class GradleBuildFileUtils { + + // Pattern for dependencies with explicit version: 'groupId:artifactId:version' + private static final Pattern DEPENDENCY_COORDINATE_PATTERN = Pattern.compile( + "(['\"])([^'\"\\s:]+):([^'\"\\s:]+):([^'\"]+)\\1" + ); + + private static final Pattern MAP_GROUP_PATTERN = Pattern.compile("group\\s*[:=]\\s*['\"]([^'\"]+)['\"]"); + private static final Pattern MAP_NAME_PATTERN = Pattern.compile("name\\s*[:=]\\s*['\"]([^'\"]+)['\"]"); + private static final Pattern MAP_VERSION_PATTERN = Pattern.compile("version\\s*[:=]\\s*['\"]?([^'\"\\s)]+)['\"]?"); + + private static final Pattern EXT_BLOCK_ASSIGNMENT_PATTERN = Pattern.compile("\\b(\\w+)\\s*=\\s*['\"]([^'\"]+)['\"]"); + private static final Pattern EXT_DOT_ASSIGNMENT_PATTERN = Pattern.compile("\\bext\\.(\\w+)\\s*=\\s*['\"]([^'\"]+)['\"]"); + private static final Pattern EXTRA_MAP_ASSIGNMENT_PATTERN = Pattern.compile("extra\\s*\\[\\s*['\"]([^'\"]+)['\"]\\s*]\\s*=\\s*['\"]([^'\"]+)['\"]"); + private static final Pattern SIMPLE_VAR_ASSIGNMENT_PATTERN = Pattern.compile("\\b(?:def|val|var)\\s+(\\w+)\\s*=\\s*['\"]([^'\"]+)['\"]"); + private static final Pattern VARIABLE_REFERENCE_PATTERN = Pattern.compile("\\$\\{(\\w+)}|\\$(\\w+)"); + + // === Spring Boot Plugin Patterns === + + // Pattern 1: Modern plugins DSL (Groovy): id 'org.springframework.boot' version '2.7.3' + private static final Pattern SPRING_BOOT_PLUGIN_VERSION_PATTERN = Pattern.compile( + "id\\s*\\(?\\s*['\"]org\\.springframework\\.boot['\"]\\s*\\)?\\s*version\\s*['\"]([^'\"]+)['\"]" + ); + + // Pattern 2: Kotlin DSL plugins: id("org.springframework.boot") version "2.7.3" + private static final Pattern SPRING_BOOT_PLUGIN_KOTLIN_PATTERN = Pattern.compile( + "id\\s*\\(\\s*['\"]org\\.springframework\\.boot['\"]\\s*\\)\\s*version\\s*['\"]([^'\"]+)['\"]" + ); + + // Pattern 3: Buildscript classpath: classpath "org.springframework.boot:spring-boot-gradle-plugin:VERSION" + private static final Pattern SPRING_BOOT_CLASSPATH_PATTERN = Pattern.compile( + "classpath\\s*\\(?\\s*['\"]org\\.springframework\\.boot:spring-boot-gradle-plugin:([^'\"]+)['\"]\\s*\\)?" + ); + + // Pattern 4: Platform/BOM: platform('org.springframework.boot:spring-boot-dependencies:2.7.3') + private static final Pattern SPRING_BOOT_PLATFORM_PATTERN = Pattern.compile( + "platform\\s*\\(\\s*['\"]org\\.springframework\\.boot:spring-boot-dependencies:([^'\"]+)['\"]\\s*\\)" + ); + + // Pattern 5: dependencyManagement mavenBom: mavenBom "org.springframework.boot:spring-boot-dependencies:2.7.3" + private static final Pattern SPRING_BOOT_MAVEN_BOM_PATTERN = Pattern.compile( + "mavenBom\\s*\\(?\\s*['\"]org\\.springframework\\.boot:spring-boot-dependencies:([^'\"]+)['\"]\\s*\\)?" + ); + + private static final Pattern JAVA_VERSION_TOKEN_PATTERN = Pattern.compile( + "JavaVersion\\.VERSION_(\\d+)(?:_(\\d+))?" + ); + + private static final Pattern JAVA_SOURCE_TARGET_PATTERN = Pattern.compile( + "(?:sourceCompatibility|targetCompatibility)\\s*=\\s*['\"]?(\\d+(?:\\.\\d+)?)['\"]?" + ); + + private static final Pattern JAVA_TOOLCHAIN_PATTERN = Pattern.compile( + "JavaLanguageVersion\\.of\\((\\d+)\\)" + ); + + private GradleBuildFileUtils() { + // Utility class, no instantiation + } + + public static boolean isGradleBuildFile(@NotNull PsiFile file) { + final String name = file.getName(); + return name.endsWith(".gradle") || name.endsWith(".gradle.kts"); + } + + /** + * Checks if the file is a Gradle Version Catalog file (libs.versions.toml). + */ + public static boolean isVersionCatalogFile(@NotNull PsiFile file) { + final String name = file.getName(); + return name.equals("libs.versions.toml"); + } + + @NotNull + public static List findDependencyLocations(@NotNull String text) { + if (text.isEmpty()) { + return Collections.emptyList(); + } + + final List locations = new ArrayList<>(); + final Map variables = extractVariables(text); + + // Find dependencies with explicit version: 'groupId:artifactId:version' + final Matcher matcher = DEPENDENCY_COORDINATE_PATTERN.matcher(text); + while (matcher.find()) { + // Skip if this match is inside a comment + if (isInsideComment(text, matcher.start())) { + continue; + } + final String groupId = matcher.group(2); + final String artifactId = matcher.group(3); + final String version = resolveVersion(matcher.group(4), variables); + locations.add(new GradleDependencyLocation(groupId, artifactId, version, matcher.start(2))); + } + + // Also check for map-style dependencies: group: 'x', name: 'y', version: 'z' + final String[] lines = text.split("\\r?\\n"); + int offset = 0; + for (String line : lines) { + // Skip commented lines + final String trimmedLine = line.trim(); + if (trimmedLine.startsWith("//") || trimmedLine.startsWith("*") || trimmedLine.startsWith("/*")) { + offset += line.length() + 1; + continue; + } + + final Matcher groupMatcher = MAP_GROUP_PATTERN.matcher(line); + final Matcher nameMatcher = MAP_NAME_PATTERN.matcher(line); + final Matcher versionMatcher = MAP_VERSION_PATTERN.matcher(line); + + if (groupMatcher.find() && nameMatcher.find() && versionMatcher.find()) { + final String groupId = groupMatcher.group(1); + final String artifactId = nameMatcher.group(1); + final String rawVersion = versionMatcher.group(1); + final String version = resolveVersion(rawVersion, variables); + locations.add(new GradleDependencyLocation(groupId, artifactId, version, offset + groupMatcher.start(1))); + } + + offset += line.length() + 1; + } + return locations; + } + + @NotNull + public static List findSpringBootPluginVersions(@NotNull String text) { + if (text.isEmpty()) { + return Collections.emptyList(); + } + + final List locations = new ArrayList<>(); + final Map variables = extractVariables(text); + + // Pattern 1: Modern plugins DSL (Groovy): id 'org.springframework.boot' version 'x.y.z' + final Matcher pluginMatcher = SPRING_BOOT_PLUGIN_VERSION_PATTERN.matcher(text); + while (pluginMatcher.find()) { + if (isInsideComment(text, pluginMatcher.start())) { + continue; + } + final String version = resolveVersion(pluginMatcher.group(1), variables); + locations.add(new GradleVersionLocation(version, pluginMatcher.start(1), pluginMatcher.end(1))); + } + + // Pattern 2: Kotlin DSL plugins: id("org.springframework.boot") version "x.y.z" + final Matcher kotlinPluginMatcher = SPRING_BOOT_PLUGIN_KOTLIN_PATTERN.matcher(text); + while (kotlinPluginMatcher.find()) { + if (isInsideComment(text, kotlinPluginMatcher.start())) { + continue; + } + final String version = resolveVersion(kotlinPluginMatcher.group(1), variables); + locations.add(new GradleVersionLocation(version, kotlinPluginMatcher.start(1), kotlinPluginMatcher.end(1))); + } + + // Pattern 3: Buildscript classpath: classpath "org.springframework.boot:spring-boot-gradle-plugin:x.y.z" + final Matcher classpathMatcher = SPRING_BOOT_CLASSPATH_PATTERN.matcher(text); + while (classpathMatcher.find()) { + if (isInsideComment(text, classpathMatcher.start())) { + continue; + } + final String version = resolveVersion(classpathMatcher.group(1), variables); + locations.add(new GradleVersionLocation(version, classpathMatcher.start(1), classpathMatcher.end(1))); + } + + // Pattern 4: Platform/BOM: platform('org.springframework.boot:spring-boot-dependencies:x.y.z') + final Matcher platformMatcher = SPRING_BOOT_PLATFORM_PATTERN.matcher(text); + while (platformMatcher.find()) { + if (isInsideComment(text, platformMatcher.start())) { + continue; + } + final String version = resolveVersion(platformMatcher.group(1), variables); + locations.add(new GradleVersionLocation(version, platformMatcher.start(1), platformMatcher.end(1))); + } + + // Pattern 5: dependencyManagement mavenBom: mavenBom "org.springframework.boot:spring-boot-dependencies:x.y.z" + final Matcher bomMatcher = SPRING_BOOT_MAVEN_BOM_PATTERN.matcher(text); + while (bomMatcher.find()) { + if (isInsideComment(text, bomMatcher.start())) { + continue; + } + final String version = resolveVersion(bomMatcher.group(1), variables); + locations.add(new GradleVersionLocation(version, bomMatcher.start(1), bomMatcher.end(1))); + } + + return locations; + } + + @NotNull + public static List findJavaVersionLocations(@NotNull String text) { + if (text.isEmpty()) { + return Collections.emptyList(); + } + + final List locations = new ArrayList<>(); + + final Matcher javaVersionMatcher = JAVA_VERSION_TOKEN_PATTERN.matcher(text); + while (javaVersionMatcher.find()) { + if (isInsideComment(text, javaVersionMatcher.start())) { + continue; + } + final String major = javaVersionMatcher.group(1); + final String minor = javaVersionMatcher.group(2); + final Integer version = parseJavaVersionToken(major, minor); + if (version != null) { + final int start = javaVersionMatcher.start(1); + final int end = minor != null ? javaVersionMatcher.end(2) : javaVersionMatcher.end(1); + locations.add(new GradleVersionLocation(String.valueOf(version), start, end)); + } + } + + final Matcher sourceTargetMatcher = JAVA_SOURCE_TARGET_PATTERN.matcher(text); + while (sourceTargetMatcher.find()) { + if (isInsideComment(text, sourceTargetMatcher.start())) { + continue; + } + final String rawVersion = sourceTargetMatcher.group(1); + final Integer version = parseSimpleVersion(rawVersion); + if (version != null) { + locations.add(new GradleVersionLocation(String.valueOf(version), sourceTargetMatcher.start(1), sourceTargetMatcher.end(1))); + } + } + + final Matcher toolchainMatcher = JAVA_TOOLCHAIN_PATTERN.matcher(text); + while (toolchainMatcher.find()) { + if (isInsideComment(text, toolchainMatcher.start())) { + continue; + } + locations.add(new GradleVersionLocation(toolchainMatcher.group(1), toolchainMatcher.start(1), toolchainMatcher.end(1))); + } + + return locations; + } + + @NotNull + public static DependencyCoordinate findDependencyAtOffset(@NotNull String text, int offset) { + final int start = Math.max(0, text.lastIndexOf('\n', offset - 1)); + final int end = Math.min(text.length(), text.indexOf('\n', offset)); + final int lineStart = start == -1 ? 0 : start + 1; + final int lineEnd = end == -1 ? text.length() : end; + final String line = text.substring(lineStart, lineEnd); + + final Matcher matcher = DEPENDENCY_COORDINATE_PATTERN.matcher(line); + while (matcher.find()) { + final int absoluteStart = lineStart + matcher.start(2); + final int absoluteEnd = lineStart + matcher.end(4); + if (offset >= absoluteStart && offset <= absoluteEnd) { + return new DependencyCoordinate(matcher.group(2), matcher.group(3), matcher.group(4)); + } + } + return DependencyCoordinate.EMPTY; + } + + private static Integer parseJavaVersionToken(@NotNull String major, String minor) { + try { + if ("1".equals(major) && minor != null) { + return Integer.parseInt(minor); + } + return Integer.parseInt(major); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static Integer parseSimpleVersion(@NotNull String version) { + try { + if (version.startsWith("1.")) { + return Integer.parseInt(version.substring(2)); + } + return Integer.parseInt(version); + } catch (NumberFormatException ignored) { + return null; + } + } + + /** + * Checks if a position in the text is inside a comment (single-line // or multi-line \/* *\/). + * + * @param text The full text content + * @param position The character position to check + * @return true if the position is inside a comment + */ + private static boolean isInsideComment(@NotNull String text, int position) { + // Find the start of the line containing this position + int lineStart = text.lastIndexOf('\n', position - 1) + 1; + String linePrefix = text.substring(lineStart, position); + + // Check for single-line comment (//) + int singleLineComment = linePrefix.indexOf("//"); + if (singleLineComment >= 0) { + return true; + } + + // Check for multi-line comment (/* ... */) + // Count /* and */ before the position + int blockCommentDepth = 0; + for (int i = 0; i < position - 1; i++) { + if (text.charAt(i) == '/' && text.charAt(i + 1) == '*') { + blockCommentDepth++; + i++; // Skip the next character + } else if (text.charAt(i) == '*' && text.charAt(i + 1) == '/') { + blockCommentDepth--; + i++; // Skip the next character + } + } + + return blockCommentDepth > 0; + } + + @NotNull + private static Map extractVariables(@NotNull String text) { + final Map variables = new HashMap<>(); + + final Matcher extBlockMatcher = Pattern.compile("ext\\s*\\{([\\s\\S]*?)}").matcher(text); + while (extBlockMatcher.find()) { + final String block = extBlockMatcher.group(1); + final Matcher assignmentMatcher = EXT_BLOCK_ASSIGNMENT_PATTERN.matcher(block); + while (assignmentMatcher.find()) { + variables.putIfAbsent(assignmentMatcher.group(1), assignmentMatcher.group(2)); + } + } + + final Matcher extDotMatcher = EXT_DOT_ASSIGNMENT_PATTERN.matcher(text); + while (extDotMatcher.find()) { + variables.putIfAbsent(extDotMatcher.group(1), extDotMatcher.group(2)); + } + + final Matcher extraMapMatcher = EXTRA_MAP_ASSIGNMENT_PATTERN.matcher(text); + while (extraMapMatcher.find()) { + variables.putIfAbsent(extraMapMatcher.group(1), extraMapMatcher.group(2)); + } + + final Matcher simpleVarMatcher = SIMPLE_VAR_ASSIGNMENT_PATTERN.matcher(text); + while (simpleVarMatcher.find()) { + variables.putIfAbsent(simpleVarMatcher.group(1), simpleVarMatcher.group(2)); + } + + return variables; + } + + @NotNull + private static String resolveVersion(@NotNull String rawVersion, @NotNull Map variables) { + final String trimmed = rawVersion.trim(); + if (variables.containsKey(trimmed)) { + return variables.get(trimmed); + } + + final Matcher matcher = VARIABLE_REFERENCE_PATTERN.matcher(trimmed); + final StringBuffer resolved = new StringBuffer(); + boolean replaced = false; + while (matcher.find()) { + final String key = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); + final String value = variables.get(key); + if (value != null) { + matcher.appendReplacement(resolved, Matcher.quoteReplacement(value)); + replaced = true; + } + } + matcher.appendTail(resolved); + + return replaced ? resolved.toString() : trimmed; + } + + /** + * Parses a Gradle Version Catalog file (libs.versions.toml) to find Spring Boot version. + * The version catalog format is: + *
+     * [versions]
+     * spring-boot = "2.7.3"
+     * 
+     * [plugins]
+     * spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
+     * 
+ * + * @param tomlContent The content of the libs.versions.toml file + * @return List of Spring Boot version locations found + */ + @NotNull + public static List findSpringBootVersionsInCatalog(@NotNull String tomlContent) { + if (tomlContent.isEmpty()) { + return Collections.emptyList(); + } + + final List locations = new ArrayList<>(); + final Map versionRefs = new HashMap<>(); + + // First pass: extract all version definitions from [versions] section + // Pattern: spring-boot = "2.7.3" or springBoot = "2.7.3" + final Pattern versionDefPattern = Pattern.compile( + "^\\s*([\\w-]+)\\s*=\\s*['\"]([^'\"]+)['\"]\\s*$", + Pattern.MULTILINE + ); + final Matcher versionMatcher = versionDefPattern.matcher(tomlContent); + while (versionMatcher.find()) { + final String key = versionMatcher.group(1); + final String version = versionMatcher.group(2); + versionRefs.put(key, new VersionLocation(version, versionMatcher.start(2), versionMatcher.end(2))); + } + + // Second pass: find Spring Boot plugin references + // Pattern: spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" } + // or: spring-boot = { id = "org.springframework.boot", version = "2.7.3" } + final Pattern pluginPattern = Pattern.compile( + "([\\w-]+)\\s*=\\s*\\{[^}]*id\\s*=\\s*['\"]org\\.springframework\\.boot['\"][^}]*\\}", + Pattern.MULTILINE + ); + final Matcher pluginMatcher = pluginPattern.matcher(tomlContent); + while (pluginMatcher.find()) { + final String pluginBlock = pluginMatcher.group(0); + + // Check for version.ref = "xxx" + final Pattern versionRefPattern = Pattern.compile("version\\.ref\\s*=\\s*['\"]([^'\"]+)['\"]"); + final Matcher refMatcher = versionRefPattern.matcher(pluginBlock); + if (refMatcher.find()) { + final String ref = refMatcher.group(1); + final VersionLocation versionLoc = versionRefs.get(ref); + if (versionLoc != null) { + locations.add(new GradleVersionLocation(versionLoc.version, versionLoc.startOffset, versionLoc.endOffset)); + } + } + + // Check for inline version = "xxx" + final Pattern inlineVersionPattern = Pattern.compile("(? locations, String version) { + return locations.stream().anyMatch(loc -> loc.version().equals(version)); + } + + private record VersionLocation(String version, int startOffset, int endOffset) {} + + public record GradleDependencyLocation(@NotNull String groupId, @NotNull String artifactId, @NotNull String version, int startOffset) { + } + + public record GradleVersionLocation(@NotNull String version, int startOffset, int endOffset) { + } + + public record DependencyCoordinate(@NotNull String groupId, @NotNull String artifactId, @NotNull String version) { + public static final DependencyCoordinate EMPTY = new DependencyCoordinate("", "", ""); + + public boolean isValid() { + return !groupId.isBlank() && !artifactId.isBlank(); + } + + public String getPackageId() { + return groupId + ":" + artifactId; + } + } +} 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 29c2ad3de5..f7b1ebcb8e 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 @@ -1,4 +1,6 @@ + org.intellij.groovy + org.jetbrains.kotlin @@ -9,7 +11,6 @@ - XML com.microsoft.azure.toolkit.intellij.appmod.javaupgrade.action.CveFixDependencyIntentionAction Azure Toolkit @@ -20,7 +21,6 @@