From fe99e4b9361d71eafd0017b3d25de715912288ec Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Wed, 10 Jun 2026 15:15:20 +0300 Subject: [PATCH 1/2] feat: read provider-level recommendations in IntelliJ plugin (TC-4696) Update the IntelliJ plugin to read recommendations from the new ProviderReport.recommendations map instead of DependencyReport.recommendation. Each recommendation appears once per recommendation source with proper attribution, eliminating per-vulnerability-source duplication. Backward compatibility with the deprecated field is maintained. Co-Authored-By: Claude Opus 4.6 --- gradle/libs.versions.toml | 2 +- .../componentanalysis/CAAnnotator.java | 201 ++++++++++++++---- .../componentanalysis/CAIntentionAction.java | 48 ++++- .../intellij/componentanalysis/CAService.java | 37 ++++ .../CAUpdateManifestIntentionAction.java | 11 + .../CAIntentionActionRecommendationTest.java | 129 +++++++++++ .../cargo/CargoCAIntentionActionTest.java | 2 + .../pypi/PyprojectCAIntentionActionTest.java | 2 + 8 files changed, 382 insertions(+), 50 deletions(-) create mode 100644 src/test/java/org/jboss/tools/intellij/componentanalysis/CAIntentionActionRecommendationTest.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f28bd72c..1a41558d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ caffeine = "3.1.8" commons-compress = "1.21" commons-io = "2.16.1" -trustify-da-api-spec = "2.0.7" +trustify-da-api-spec = "2.0.8" trustify-da-java-client = "0.0.17" github-api = "1.314" junit = "4.13.2" diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java index 93ea1f45..bdfba492 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java @@ -32,6 +32,7 @@ import com.intellij.serviceContainer.AlreadyDisposedException; import io.github.guacsec.trustifyda.api.v5.DependencyReport; import io.github.guacsec.trustifyda.api.v5.LicenseIdentifier; +import io.github.guacsec.trustifyda.api.v5.RecommendationReport; import io.github.guacsec.trustifyda.license.LicenseCheck.IncompatibleDependency; import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary; import io.github.guacsec.trustifyda.license.LicenseCheck.ProjectLicenseSummary; @@ -40,6 +41,7 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -105,7 +107,8 @@ public abstract class CAAnnotator extends ExternalAnnotator> reports = CAService.getReports(path); - Map dependencyResultMap = this.matchDependencies(info.getDependencies(), reports); + Map> recommendations = CAService.getRecommendations(path); + Map dependencyResultMap = this.matchDependencies(info.getDependencies(), reports, recommendations); return new AnnotationData(dependencyResultMap, info.getDependencies()); } @@ -137,19 +140,39 @@ public void apply(@NotNull PsiFile file, AnnotationData annotationData, @NotNull annotationResult.forEach((key, value) -> { if (value != null) { Map reports = value.getReports(); + Map recommendations = value.getRecommendations(); List elements = value.getElements(); - if (reports != null && !reports.isEmpty() + boolean hasReports = reports != null && !reports.isEmpty(); + boolean hasRecommendations = recommendations != null && !recommendations.isEmpty(); + + if ((hasReports || hasRecommendations) && elements != null && !elements.isEmpty()) { - Optional reportOptional = reports.values().stream() - .filter(Objects::nonNull).findAny(); - if (reportOptional.isPresent() && reportOptional.get().getRef() != null) { - String name = getDependencyString(reportOptional.get().getRef().purl()); + // Determine the dependency name from reports or recommendations + String name = null; + if (hasReports) { + Optional reportOptional = reports.values().stream() + .filter(Objects::nonNull).findAny(); + if (reportOptional.isPresent() && reportOptional.get().getRef() != null) { + name = getDependencyString(reportOptional.get().getRef().purl()); + } + } + if (name == null && hasRecommendations) { + Optional recOptional = recommendations.values().stream() + .filter(r -> r != null && r.getRef() != null).findAny(); + if (recOptional.isPresent()) { + name = getDependencyString(recOptional.get().getRef().purl()); + } + } + if (name == null) { + return; + } - StringBuilder messageBuilder = new StringBuilder(name); - StringBuilder tooltipBuilder = new StringBuilder("").append("

").append(escapeHtml(name)).append("

"); - Map quickfixes = new HashMap<>(); + StringBuilder messageBuilder = new StringBuilder(name); + StringBuilder tooltipBuilder = new StringBuilder("").append("

").append(escapeHtml(name)).append("

"); + Map quickfixes = new HashMap<>(); + if (hasReports) { reports.forEach((source, report) -> { if (report.getIssues() != null && !report.getIssues().isEmpty()) { messageBuilder.append(System.lineSeparator()); @@ -190,45 +213,99 @@ public void apply(@NotNull PsiFile file, AnnotationData annotationData, @NotNull } } - if (CAIntentionAction.isQuickFixAvailable(report) || !CAIntentionAction.thereAreNoIssues(report)) { + // Only add vulnerability-based quickfixes (TC remediation or legacy recommendation fallback) + if (!CAIntentionAction.thereAreNoIssues(report)) { + quickfixes.put(source, report); + } else if (!hasRecommendations && CAIntentionAction.isQuickFixAvailable(report)) { + // Fallback: use DependencyReport.recommendation only when provider-level is absent quickfixes.put(source, report); } }); + } - // Merge license incompatibility info into the annotation - Dependency noVersionKey = new Dependency(key, false); - String licenseMsg = licenseMessages.get(noVersionKey); - if (licenseMsg != null) { - messageBuilder.append(System.lineSeparator()).append(licenseMsg); - tooltipBuilder.append("

").append(licenseTooltips.get(noVersionKey)); - annotatedLicenseDeps.add(noVersionKey); - } + // Add provider-level recommendation info to annotation + if (hasRecommendations) { + recommendations.forEach((recSourceName, recReport) -> { + if (CAIntentionAction.isQuickFixAvailable(recReport)) { + messageBuilder.append(System.lineSeparator()); + tooltipBuilder.append("

"); + messageBuilder.append(recSourceName) + .append(" recommendation: version ") + .append(recReport.getRecommendation().version()); + tooltipBuilder.append("

") + .append(escapeHtml(recSourceName)) + .append(" recommendation: version ") + .append(escapeHtml(recReport.getRecommendation().version())) + .append("

"); + } + }); + } + + // Merge license incompatibility info into the annotation + Dependency noVersionKey = new Dependency(key, false); + String licenseMsg = licenseMessages.get(noVersionKey); + if (licenseMsg != null) { + messageBuilder.append(System.lineSeparator()).append(licenseMsg); + tooltipBuilder.append("

").append(licenseTooltips.get(noVersionKey)); + annotatedLicenseDeps.add(noVersionKey); + } - elements.forEach(e -> { - if (e != null) { - if (!quickfixes.isEmpty() && this.isQuickFixApplicable(e)) { - DependencyReport firstReport = quickfixes.values().iterator().next(); - AnnotationBuilder builder = holder - .newAnnotation(getHighlightSeverity(firstReport, e), messageBuilder.toString()) + // Determine severity from first available report + final DependencyReport severityReport = hasReports + ? reports.values().stream().filter(Objects::nonNull).findAny().orElse(null) + : null; + + elements.forEach(e -> { + if (e != null) { + boolean hasQuickfixes = !quickfixes.isEmpty() || hasRecommendations; + if (hasQuickfixes && this.isQuickFixApplicable(e)) { + AnnotationBuilder builder; + if (severityReport != null) { + builder = holder + .newAnnotation(getHighlightSeverity(severityReport, e), messageBuilder.toString()) .tooltip(tooltipBuilder.toString()) .range(e); + } else { + // Recommendation-only: use WEAK_WARNING + builder = holder + .newAnnotation(HighlightSeverity.WEAK_WARNING, messageBuilder.toString()) + .tooltip(tooltipBuilder.toString()) + .range(e); + } - quickfixes.forEach((source, report) -> { - if(CAIntentionAction.isQuickFixAvailable(report)) { - builder.withFix(this.createQuickFix(e, source, report)); - CAUpdateManifestIntentionAction patchManifest = this.patchManifest(file, report); - if(Objects.nonNull(patchManifest)) { - builder.withFix(patchManifest); - } + // Add vulnerability-based quickfixes (TC remediation or legacy recommendation fallback) + quickfixes.forEach((source, report) -> { + if(CAIntentionAction.isQuickFixAvailable(report)) { + builder.withFix(this.createQuickFix(e, source, report)); + CAUpdateManifestIntentionAction patchManifest = this.patchManifest(file, report); + if(Objects.nonNull(patchManifest)) { + builder.withFix(patchManifest); + } + } + }); + + // Add provider-level recommendation quickfixes (one per recommendation source) + if (hasRecommendations) { + recommendations.forEach((recSourceName, recReport) -> { + if (CAIntentionAction.isQuickFixAvailable(recReport)) { + // Use first available DependencyReport as basis for the quickfix + DependencyReport basisReport = hasReports + ? reports.values().iterator().next() + : createSyntheticReport(recReport); + VulnerabilitySource syntheticSource = new VulnerabilitySource(recSourceName, recSourceName); + CAIntentionAction recAction = this.createQuickFix(e, syntheticSource, basisReport); + recAction.setRecommendationData(recSourceName, recReport); + builder.withFix(recAction); } }); - builder.withFix(new SAIntentionAction()); - builder.withFix(new ExcludeManifestIntentionAction()); - builder.create(); } + + builder.withFix(new SAIntentionAction()); + builder.withFix(new ExcludeManifestIntentionAction()); + builder.create(); } - }); - } + } + }); } } }); @@ -464,6 +541,14 @@ private HighlightSeverity getHighlightSeverity(DependencyReport report, @NotNull return HighlightSeverity.ERROR; } + /** Creates a synthetic DependencyReport from a RecommendationReport for quickfix creation. */ + private static DependencyReport createSyntheticReport(RecommendationReport recReport) { + DependencyReport synthetic = new DependencyReport(); + synthetic.setRef(recReport.getRef()); + synthetic.setRecommendation(recReport.getRecommendation()); + return synthetic; + } + abstract protected String getInspectionShortName(); abstract protected Map> getDependencies(PsiFile file); @@ -477,18 +562,28 @@ private HighlightSeverity getHighlightSeverity(DependencyReport report, @NotNull abstract protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense); private Map matchDependencies(Map> dependencies, - Map> reports) { - if (dependencies != null && !dependencies.isEmpty() - && reports != null && !reports.isEmpty()) { - return dependencies.entrySet() - .parallelStream() - .filter(e -> reports.containsKey(e.getKey())) - .collect(Collectors.toMap( - Map.Entry::getKey, - e -> new Result(dependencies.get(e.getKey()), reports.get(e.getKey())), - (o1, o2) -> o1)); + Map> reports, + Map> recommendations) { + if (dependencies == null || dependencies.isEmpty()) { + return null; } - return null; + boolean hasReports = reports != null && !reports.isEmpty(); + boolean hasRecommendations = recommendations != null && !recommendations.isEmpty(); + if (!hasReports && !hasRecommendations) { + return null; + } + + return dependencies.entrySet() + .parallelStream() + .filter(e -> (hasReports && reports.containsKey(e.getKey())) + || (hasRecommendations && recommendations.containsKey(e.getKey()))) + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> new Result( + dependencies.get(e.getKey()), + hasReports ? reports.getOrDefault(e.getKey(), Collections.emptyMap()) : Collections.emptyMap(), + hasRecommendations ? recommendations.getOrDefault(e.getKey(), null) : null), + (o1, o2) -> o1)); } private String getDependencyString(PackageURL purl) { @@ -552,12 +647,20 @@ public Map> getDependencies() { public static class Result { List elements; Map reports; + Map recommendations; public Result(List elements, Map reports) { this.elements = elements; this.reports = reports; } + public Result(List elements, Map reports, + Map recommendations) { + this.elements = elements; + this.reports = reports; + this.recommendations = recommendations; + } + public List getElements() { return elements; } @@ -565,6 +668,10 @@ public List getElements() { public Map getReports() { return reports; } + + public Map getRecommendations() { + return recommendations; + } } public static class AnnotationData { diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java index 7a15d5ab..58ddc5cc 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java @@ -23,6 +23,7 @@ import com.intellij.util.IncorrectOperationException; import io.github.guacsec.trustifyda.api.v5.DependencyReport; import io.github.guacsec.trustifyda.api.v5.Issue; +import io.github.guacsec.trustifyda.api.v5.RecommendationReport; import org.jboss.tools.intellij.exhort.TelemetryService; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,6 +35,8 @@ public abstract class CAIntentionAction implements IntentionAction { protected @SafeFieldForPreview PsiElement element; protected @SafeFieldForPreview VulnerabilitySource source; protected @SafeFieldForPreview DependencyReport report; + protected @SafeFieldForPreview String recommendationSourceName; + protected @SafeFieldForPreview RecommendationReport recommendationReport; protected CAIntentionAction(PsiElement element, VulnerabilitySource source, DependencyReport report) { this.element = element; @@ -41,8 +44,17 @@ protected CAIntentionAction(PsiElement element, VulnerabilitySource source, Depe this.report = report; } + /** Sets provider-level recommendation data for source-attributed quick-fixes. */ + void setRecommendationData(String sourceName, RecommendationReport recReport) { + this.recommendationSourceName = sourceName; + this.recommendationReport = recReport; + } + @Override public @IntentionName @NotNull String getText() { + if (recommendationSourceName != null) { + return getQuickFixTextForRecommendation(recommendationSourceName); + } return getQuickFixText(this.source, this.report); } @@ -53,12 +65,24 @@ protected CAIntentionAction(PsiElement element, VulnerabilitySource source, Depe @Override public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { - String recommendedVersion = getRecommendedVersion(this.report); + String recommendedVersion; + if (recommendationReport != null && recommendationReport.getRecommendation() != null) { + recommendedVersion = recommendationReport.getRecommendation().version(); + } else { + recommendedVersion = getRecommendedVersion(this.report); + } this.updateVersion(project, editor, file, recommendedVersion); TelemetryService.sendPackageUpdateEvent(file, recommendedVersion, this.report.getRef().name(), "recommendation-accepted"); } private String getRecommendationsRepo(DependencyReport dependency) { + // Check provider-level recommendation first + if (recommendationReport != null && recommendationReport.getRecommendation() != null + && recommendationReport.getRecommendation().purl() != null + && recommendationReport.getRecommendation().purl().getQualifiers() != null) { + return recommendationReport.getRecommendation().purl().getQualifiers().get("repository_url"); + } + String repo=null; if(thereAreNoIssues(dependency)) { @@ -86,7 +110,11 @@ public boolean startInWriteAction() { @Override public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) { - return this.createCAIntentionActionInCopy(PsiTreeUtil.findSameElementInCopy(this.element, target)); + FileModifier modifier = this.createCAIntentionActionInCopy(PsiTreeUtil.findSameElementInCopy(this.element, target)); + if (modifier instanceof CAIntentionAction copy && this.recommendationSourceName != null) { + copy.setRecommendationData(this.recommendationSourceName, this.recommendationReport); + } + return modifier; } protected abstract void updateVersion(@NotNull Project project, Editor editor, PsiFile file, String version); @@ -129,6 +157,13 @@ static boolean thereIsRecommendation(DependencyReport dependency) { return dependency.getRecommendation() != null && !dependency.getRecommendation().version().trim().isEmpty(); } + /** Checks if a provider-level recommendation report has a valid recommendation. */ + static boolean thereIsRecommendation(RecommendationReport recReport) { + return recReport != null && recReport.getRecommendation() != null + && recReport.getRecommendation().version() != null + && !recReport.getRecommendation().version().trim().isEmpty(); + } + static boolean thereAreNoIssues(DependencyReport dependency) { return dependency.getIssues() == null || dependency.getIssues().size() == 0; } @@ -149,6 +184,10 @@ static boolean thereAreNoIssues(DependencyReport dependency) { return text; } + private static @NotNull String getQuickFixTextForRecommendation(String recommendationSourceName) { + return "Quick-Fix suggestion (" + recommendationSourceName + ") - apply redhat Recommended version"; + } + static boolean isQuickFixAvailable(DependencyReport dependency) { boolean result=false; if(thereAreNoIssues(dependency)) @@ -167,4 +206,9 @@ static boolean isQuickFixAvailable(DependencyReport dependency) { } return result; } + + /** Checks if a provider-level recommendation report has an available quick-fix. */ + static boolean isQuickFixAvailable(RecommendationReport recReport) { + return thereIsRecommendation(recReport); + } } diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java index b786af68..44e66763 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java @@ -27,6 +27,8 @@ import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import io.github.guacsec.trustifyda.api.v5.DependencyReport; import io.github.guacsec.trustifyda.api.v5.ProviderReport; +import io.github.guacsec.trustifyda.api.v5.RecommendationReport; +import io.github.guacsec.trustifyda.api.v5.RecommendationSource; import io.github.guacsec.trustifyda.api.v5.Source; import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary; import org.jboss.tools.intellij.exhort.ApiService; @@ -53,6 +55,10 @@ public static CAService getInstance() { .maximumSize(100) .build(); + private final Cache>> recommendationCache = Caffeine.newBuilder() + .maximumSize(100) + .build(); + private final Cache> dependencyCache = Caffeine.newBuilder() .expireAfterWrite(25, TimeUnit.SECONDS) .maximumSize(100) @@ -66,14 +72,21 @@ public static Map> getRep return Collections.unmodifiableMap(getInstance().vulnerabilityCache.get(filePath, p -> Collections.emptyMap())); } + /** Returns provider-level recommendations keyed by dependency and recommendation source name. */ + public static Map> getRecommendations(String filePath) { + return Collections.unmodifiableMap(getInstance().recommendationCache.get(filePath, p -> Collections.emptyMap())); + } + public static void deleteReports(String filePath) { getInstance().vulnerabilityCache.invalidate(filePath); + getInstance().recommendationCache.invalidate(filePath); getInstance().licenseCache.invalidate(filePath); getInstance().dependencyCache.invalidate(filePath); } public static void invalidateAllCaches() { getInstance().vulnerabilityCache.invalidateAll(); + getInstance().recommendationCache.invalidateAll(); getInstance().licenseCache.invalidateAll(); getInstance().dependencyCache.invalidateAll(); } @@ -126,6 +139,7 @@ public static boolean performAnalysis(String packageManager, } Map> resultMap = new ConcurrentHashMap<>(); + Map> recommendationMap = new ConcurrentHashMap<>(); if (report.getProviders() != null) { // Avoid comparing the version of dependency @@ -183,6 +197,24 @@ public static boolean performAnalysis(String packageManager, } }); } + + // Process provider-level recommendations + if (providerReport.getRecommendations() != null) { + providerReport.getRecommendations().forEach((recSourceName, recSource) -> { + if (recSource.getDependencies() != null) { + for (RecommendationReport recReport : recSource.getDependencies()) { + if (recReport.getRef() != null) { + Dependency recDep = new Dependency(recReport.getRef().purl(), false); + dependencyMap.entrySet().stream() + .filter(e -> recDep.equals(e.getValue())) + .forEach(e -> recommendationMap + .computeIfAbsent(e.getKey(), key -> new ConcurrentHashMap<>()) + .put(recSourceName, recReport)); + } + } + } + }); + } }); } @@ -191,6 +223,11 @@ public static boolean performAnalysis(String packageManager, } else { getInstance().vulnerabilityCache.invalidate(filePath); } + if (!recommendationMap.isEmpty()) { + getInstance().recommendationCache.put(filePath, recommendationMap); + } else { + getInstance().recommendationCache.invalidate(filePath); + } getInstance().dependencyCache.put(filePath, dependencies); return true; diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAUpdateManifestIntentionAction.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAUpdateManifestIntentionAction.java index cd6a4867..80c6c63d 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAUpdateManifestIntentionAction.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAUpdateManifestIntentionAction.java @@ -21,6 +21,7 @@ import com.intellij.util.IncorrectOperationException; import io.github.guacsec.trustifyda.api.PackageRef; import io.github.guacsec.trustifyda.api.v5.DependencyReport; +import io.github.guacsec.trustifyda.api.v5.RecommendationReport; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -36,6 +37,16 @@ protected static String getRepositoryUrl(DependencyReport dependency) { return getRepositoryUrlFromPurl(dependency); } + /** Extracts the repository URL from a provider-level recommendation report. */ + protected static String getRepositoryUrl(RecommendationReport recReport) { + if (recReport != null && recReport.getRecommendation() != null + && recReport.getRecommendation().purl() != null + && recReport.getRecommendation().purl().getQualifiers() != null) { + return recReport.getRecommendation().purl().getQualifiers().get("repository_url"); + } + return null; + } + protected static String getRepositoryUrlFromPurl(DependencyReport dependency) { AtomicReference packageRef = new AtomicReference<>(); if(Objects.nonNull(dependency.getRecommendation())) diff --git a/src/test/java/org/jboss/tools/intellij/componentanalysis/CAIntentionActionRecommendationTest.java b/src/test/java/org/jboss/tools/intellij/componentanalysis/CAIntentionActionRecommendationTest.java new file mode 100644 index 00000000..4e202c56 --- /dev/null +++ b/src/test/java/org/jboss/tools/intellij/componentanalysis/CAIntentionActionRecommendationTest.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2025 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ + +package org.jboss.tools.intellij.componentanalysis; + +import io.github.guacsec.trustifyda.api.PackageRef; +import io.github.guacsec.trustifyda.api.v5.DependencyReport; +import io.github.guacsec.trustifyda.api.v5.RecommendationReport; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for provider-level recommendation support in CAIntentionAction. + * Placed in the base package to access package-private methods. + */ +public class CAIntentionActionRecommendationTest { + + private static final String NEW_VERSION = "4.0.0"; + + private RecommendationReport createProviderLevelRecommendation() { + RecommendationReport recReport = new RecommendationReport(); + recReport.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0")); + recReport.setRecommendation(new PackageRef("pkg:maven/com.example/test@" + NEW_VERSION)); + return recReport; + } + + private DependencyReport createReportWithLegacyRecommendation() { + DependencyReport report = new DependencyReport(); + report.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0")); + report.setRecommendation(new PackageRef("pkg:maven/com.example/test@" + NEW_VERSION)); + return report; + } + + private DependencyReport createReportWithoutRecommendation() { + DependencyReport report = new DependencyReport(); + report.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0")); + return report; + } + + // ── thereIsRecommendation tests ──────────────────────────────────────────── + + /** Verifies that thereIsRecommendation detects a valid provider-level recommendation. */ + @Test + public void testThereIsRecommendationWithProviderLevel() { + RecommendationReport recReport = createProviderLevelRecommendation(); + assertTrue("Should detect provider-level recommendation", + CAIntentionAction.thereIsRecommendation(recReport)); + } + + /** Verifies that thereIsRecommendation returns false for null RecommendationReport. */ + @Test + public void testThereIsRecommendationWithNull() { + assertFalse("Should return false for null", + CAIntentionAction.thereIsRecommendation((RecommendationReport) null)); + } + + /** Verifies that thereIsRecommendation returns false for empty version. */ + @Test + public void testThereIsRecommendationWithEmptyVersion() { + RecommendationReport recReport = new RecommendationReport(); + recReport.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0")); + recReport.setRecommendation(new PackageRef("pkg:maven/com.example/test@")); + assertFalse("Should return false for empty version", + CAIntentionAction.thereIsRecommendation(recReport)); + } + + /** Verifies that thereIsRecommendation still works with legacy DependencyReport path. */ + @Test + public void testThereIsRecommendationWithLegacyReport() { + DependencyReport report = createReportWithLegacyRecommendation(); + assertTrue("Should detect legacy recommendation", + CAIntentionAction.thereIsRecommendation(report)); + } + + /** Verifies that thereIsRecommendation returns false for DependencyReport without recommendation. */ + @Test + public void testThereIsRecommendationFalseWithLegacyReport() { + DependencyReport report = createReportWithoutRecommendation(); + assertFalse("Should return false when no recommendation set", + CAIntentionAction.thereIsRecommendation(report)); + } + + // ── isQuickFixAvailable tests ────────────────────────────────────────────── + + /** Verifies that isQuickFixAvailable returns true for a valid provider-level recommendation. */ + @Test + public void testIsQuickFixAvailableForProviderLevel() { + RecommendationReport recReport = createProviderLevelRecommendation(); + assertTrue("Should be available for provider-level recommendation", + CAIntentionAction.isQuickFixAvailable(recReport)); + } + + /** Verifies that isQuickFixAvailable returns false for null RecommendationReport. */ + @Test + public void testIsQuickFixAvailableForNull() { + assertFalse("Should not be available for null", + CAIntentionAction.isQuickFixAvailable((RecommendationReport) null)); + } + + /** Verifies that isQuickFixAvailable returns true for legacy recommendation (no issues). */ + @Test + public void testIsQuickFixAvailableForLegacyRecommendation() { + DependencyReport report = createReportWithLegacyRecommendation(); + assertTrue("Should be available for legacy recommendation", + CAIntentionAction.isQuickFixAvailable(report)); + } + + // ── Version extraction tests ─────────────────────────────────────────────── + + /** Verifies that provider-level recommendation version is correctly extractable. */ + @Test + public void testRecommendationVersionExtraction() { + RecommendationReport recReport = createProviderLevelRecommendation(); + assertNotNull("Recommendation should not be null", recReport.getRecommendation()); + assertEquals("Version should match", NEW_VERSION, recReport.getRecommendation().version()); + } +} diff --git a/src/test/java/org/jboss/tools/intellij/componentanalysis/cargo/CargoCAIntentionActionTest.java b/src/test/java/org/jboss/tools/intellij/componentanalysis/cargo/CargoCAIntentionActionTest.java index 2145b0d3..0a92d16b 100644 --- a/src/test/java/org/jboss/tools/intellij/componentanalysis/cargo/CargoCAIntentionActionTest.java +++ b/src/test/java/org/jboss/tools/intellij/componentanalysis/cargo/CargoCAIntentionActionTest.java @@ -33,6 +33,7 @@ public class CargoCAIntentionActionTest extends BasePlatformTestCase { private DependencyReport createReportWithRecommendation() { DependencyReport report = new DependencyReport(); + report.setRef(new PackageRef("pkg:cargo/test-crate@1.0.0")); report.setRecommendation(new PackageRef("pkg:cargo/test-crate@" + CargoCAIntentionActionTest.NEW_VERSION)); return report; } @@ -376,4 +377,5 @@ public void testUpdateTargetSpecificComplexObjectDependency() { assertTrue("winapi features should remain unchanged", updatedText.contains("features = [\"winuser\"]")); } + } diff --git a/src/test/java/org/jboss/tools/intellij/componentanalysis/pypi/PyprojectCAIntentionActionTest.java b/src/test/java/org/jboss/tools/intellij/componentanalysis/pypi/PyprojectCAIntentionActionTest.java index 8c459b9c..3acc15bc 100644 --- a/src/test/java/org/jboss/tools/intellij/componentanalysis/pypi/PyprojectCAIntentionActionTest.java +++ b/src/test/java/org/jboss/tools/intellij/componentanalysis/pypi/PyprojectCAIntentionActionTest.java @@ -33,6 +33,7 @@ public class PyprojectCAIntentionActionTest extends BasePlatformTestCase { private DependencyReport createReportWithRecommendation() { DependencyReport report = new DependencyReport(); + report.setRef(new PackageRef("pkg:pypi/test-package@3.0.0")); report.setRecommendation(new PackageRef("pkg:pypi/test-package@" + NEW_VERSION)); return report; } @@ -261,4 +262,5 @@ public void testIsNotAvailableForOtherTomlFiles() { assertFalse("Should not be available for Cargo.toml", action.isAvailable(getProject(), null, file)); } + } From ad04de803559a13f866addc64112d8d795c8c6d2 Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Mon, 15 Jun 2026 11:25:42 +0300 Subject: [PATCH 2/2] fix: capitalize Red Hat in quick-fix suggestion text Co-Authored-By: Claude Opus 4.6 --- .../tools/intellij/componentanalysis/CAIntentionAction.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java index 58ddc5cc..0db53b13 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java @@ -172,20 +172,20 @@ static boolean thereAreNoIssues(DependencyReport dependency) { String text=""; if(thereAreNoIssues(dependency) && thereIsRecommendation(dependency)) { - text = "Quick-Fix suggestion - apply redhat Recommended version"; + text = "Quick-Fix suggestion - apply Red Hat Recommended version"; } else { if(thereIsTcRemediation(dependency)) { - text = "Quick-Fix suggestion - apply redhat remediation version"; + text = "Quick-Fix suggestion - apply Red Hat remediation version"; } } return text; } private static @NotNull String getQuickFixTextForRecommendation(String recommendationSourceName) { - return "Quick-Fix suggestion (" + recommendationSourceName + ") - apply redhat Recommended version"; + return "Quick-Fix suggestion (" + recommendationSourceName + ") - apply Red Hat Recommended version"; } static boolean isQuickFixAvailable(DependencyReport dependency) {