Skip to content

Commit 102ce59

Browse files
a-orenclaude
andauthored
feat: read provider-level recommendations in IntelliJ plugin (TC-4696) (#259)
* 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 <noreply@anthropic.com> * fix: capitalize Red Hat in quick-fix suggestion text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 87ab401 commit 102ce59

8 files changed

Lines changed: 384 additions & 52 deletions

File tree

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
caffeine = "3.1.8"
44
commons-compress = "1.21"
55
commons-io = "2.16.1"
6-
trustify-da-api-spec = "2.0.7"
6+
trustify-da-api-spec = "2.0.8"
77
trustify-da-java-client = "0.0.17"
88
github-api = "1.314"
99
junit = "4.13.2"

src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java

Lines changed: 154 additions & 47 deletions
Large diffs are not rendered by default.

src/main/java/org/jboss/tools/intellij/componentanalysis/CAIntentionAction.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.intellij.util.IncorrectOperationException;
2424
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
2525
import io.github.guacsec.trustifyda.api.v5.Issue;
26+
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
2627
import org.jboss.tools.intellij.exhort.TelemetryService;
2728
import org.jetbrains.annotations.NotNull;
2829
import org.jetbrains.annotations.Nullable;
@@ -34,15 +35,26 @@ public abstract class CAIntentionAction implements IntentionAction {
3435
protected @SafeFieldForPreview PsiElement element;
3536
protected @SafeFieldForPreview VulnerabilitySource source;
3637
protected @SafeFieldForPreview DependencyReport report;
38+
protected @SafeFieldForPreview String recommendationSourceName;
39+
protected @SafeFieldForPreview RecommendationReport recommendationReport;
3740

3841
protected CAIntentionAction(PsiElement element, VulnerabilitySource source, DependencyReport report) {
3942
this.element = element;
4043
this.source = source;
4144
this.report = report;
4245
}
4346

47+
/** Sets provider-level recommendation data for source-attributed quick-fixes. */
48+
void setRecommendationData(String sourceName, RecommendationReport recReport) {
49+
this.recommendationSourceName = sourceName;
50+
this.recommendationReport = recReport;
51+
}
52+
4453
@Override
4554
public @IntentionName @NotNull String getText() {
55+
if (recommendationSourceName != null) {
56+
return getQuickFixTextForRecommendation(recommendationSourceName);
57+
}
4658
return getQuickFixText(this.source, this.report);
4759
}
4860

@@ -53,12 +65,24 @@ protected CAIntentionAction(PsiElement element, VulnerabilitySource source, Depe
5365

5466
@Override
5567
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
56-
String recommendedVersion = getRecommendedVersion(this.report);
68+
String recommendedVersion;
69+
if (recommendationReport != null && recommendationReport.getRecommendation() != null) {
70+
recommendedVersion = recommendationReport.getRecommendation().version();
71+
} else {
72+
recommendedVersion = getRecommendedVersion(this.report);
73+
}
5774
this.updateVersion(project, editor, file, recommendedVersion);
5875
TelemetryService.sendPackageUpdateEvent(file, recommendedVersion, this.report.getRef().name(), "recommendation-accepted");
5976
}
6077

6178
private String getRecommendationsRepo(DependencyReport dependency) {
79+
// Check provider-level recommendation first
80+
if (recommendationReport != null && recommendationReport.getRecommendation() != null
81+
&& recommendationReport.getRecommendation().purl() != null
82+
&& recommendationReport.getRecommendation().purl().getQualifiers() != null) {
83+
return recommendationReport.getRecommendation().purl().getQualifiers().get("repository_url");
84+
}
85+
6286
String repo=null;
6387
if(thereAreNoIssues(dependency))
6488
{
@@ -86,7 +110,11 @@ public boolean startInWriteAction() {
86110

87111
@Override
88112
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
89-
return this.createCAIntentionActionInCopy(PsiTreeUtil.findSameElementInCopy(this.element, target));
113+
FileModifier modifier = this.createCAIntentionActionInCopy(PsiTreeUtil.findSameElementInCopy(this.element, target));
114+
if (modifier instanceof CAIntentionAction copy && this.recommendationSourceName != null) {
115+
copy.setRecommendationData(this.recommendationSourceName, this.recommendationReport);
116+
}
117+
return modifier;
90118
}
91119

92120
protected abstract void updateVersion(@NotNull Project project, Editor editor, PsiFile file, String version);
@@ -129,6 +157,13 @@ static boolean thereIsRecommendation(DependencyReport dependency) {
129157
return dependency.getRecommendation() != null && !dependency.getRecommendation().version().trim().isEmpty();
130158
}
131159

160+
/** Checks if a provider-level recommendation report has a valid recommendation. */
161+
static boolean thereIsRecommendation(RecommendationReport recReport) {
162+
return recReport != null && recReport.getRecommendation() != null
163+
&& recReport.getRecommendation().version() != null
164+
&& !recReport.getRecommendation().version().trim().isEmpty();
165+
}
166+
132167
static boolean thereAreNoIssues(DependencyReport dependency) {
133168
return dependency.getIssues() == null || dependency.getIssues().size() == 0;
134169
}
@@ -137,18 +172,22 @@ static boolean thereAreNoIssues(DependencyReport dependency) {
137172
String text="";
138173
if(thereAreNoIssues(dependency) && thereIsRecommendation(dependency))
139174
{
140-
text = "Quick-Fix suggestion - apply redhat Recommended version";
175+
text = "Quick-Fix suggestion - apply Red Hat Recommended version";
141176
}
142177
else
143178
{
144179
if(thereIsTcRemediation(dependency))
145180
{
146-
text = "Quick-Fix suggestion - apply redhat remediation version";
181+
text = "Quick-Fix suggestion - apply Red Hat remediation version";
147182
}
148183
}
149184
return text;
150185
}
151186

187+
private static @NotNull String getQuickFixTextForRecommendation(String recommendationSourceName) {
188+
return "Quick-Fix suggestion (" + recommendationSourceName + ") - apply Red Hat Recommended version";
189+
}
190+
152191
static boolean isQuickFixAvailable(DependencyReport dependency) {
153192
boolean result=false;
154193
if(thereAreNoIssues(dependency))
@@ -167,4 +206,9 @@ static boolean isQuickFixAvailable(DependencyReport dependency) {
167206
}
168207
return result;
169208
}
209+
210+
/** Checks if a provider-level recommendation report has an available quick-fix. */
211+
static boolean isQuickFixAvailable(RecommendationReport recReport) {
212+
return thereIsRecommendation(recReport);
213+
}
170214
}

src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import io.github.guacsec.trustifyda.api.v5.AnalysisReport;
2828
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
2929
import io.github.guacsec.trustifyda.api.v5.ProviderReport;
30+
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
31+
import io.github.guacsec.trustifyda.api.v5.RecommendationSource;
3032
import io.github.guacsec.trustifyda.api.v5.Source;
3133
import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary;
3234
import org.jboss.tools.intellij.exhort.ApiService;
@@ -53,6 +55,10 @@ public static CAService getInstance() {
5355
.maximumSize(100)
5456
.build();
5557

58+
private final Cache<String, Map<Dependency, Map<String, RecommendationReport>>> recommendationCache = Caffeine.newBuilder()
59+
.maximumSize(100)
60+
.build();
61+
5662
private final Cache<String, Set<Dependency>> dependencyCache = Caffeine.newBuilder()
5763
.expireAfterWrite(25, TimeUnit.SECONDS)
5864
.maximumSize(100)
@@ -66,14 +72,21 @@ public static Map<Dependency, Map<VulnerabilitySource, DependencyReport>> getRep
6672
return Collections.unmodifiableMap(getInstance().vulnerabilityCache.get(filePath, p -> Collections.emptyMap()));
6773
}
6874

75+
/** Returns provider-level recommendations keyed by dependency and recommendation source name. */
76+
public static Map<Dependency, Map<String, RecommendationReport>> getRecommendations(String filePath) {
77+
return Collections.unmodifiableMap(getInstance().recommendationCache.get(filePath, p -> Collections.emptyMap()));
78+
}
79+
6980
public static void deleteReports(String filePath) {
7081
getInstance().vulnerabilityCache.invalidate(filePath);
82+
getInstance().recommendationCache.invalidate(filePath);
7183
getInstance().licenseCache.invalidate(filePath);
7284
getInstance().dependencyCache.invalidate(filePath);
7385
}
7486

7587
public static void invalidateAllCaches() {
7688
getInstance().vulnerabilityCache.invalidateAll();
89+
getInstance().recommendationCache.invalidateAll();
7790
getInstance().licenseCache.invalidateAll();
7891
getInstance().dependencyCache.invalidateAll();
7992
}
@@ -126,6 +139,7 @@ public static boolean performAnalysis(String packageManager,
126139
}
127140

128141
Map<Dependency, Map<VulnerabilitySource, DependencyReport>> resultMap = new ConcurrentHashMap<>();
142+
Map<Dependency, Map<String, RecommendationReport>> recommendationMap = new ConcurrentHashMap<>();
129143

130144
if (report.getProviders() != null) {
131145
// Avoid comparing the version of dependency
@@ -183,6 +197,24 @@ public static boolean performAnalysis(String packageManager,
183197
}
184198
});
185199
}
200+
201+
// Process provider-level recommendations
202+
if (providerReport.getRecommendations() != null) {
203+
providerReport.getRecommendations().forEach((recSourceName, recSource) -> {
204+
if (recSource.getDependencies() != null) {
205+
for (RecommendationReport recReport : recSource.getDependencies()) {
206+
if (recReport.getRef() != null) {
207+
Dependency recDep = new Dependency(recReport.getRef().purl(), false);
208+
dependencyMap.entrySet().stream()
209+
.filter(e -> recDep.equals(e.getValue()))
210+
.forEach(e -> recommendationMap
211+
.computeIfAbsent(e.getKey(), key -> new ConcurrentHashMap<>())
212+
.put(recSourceName, recReport));
213+
}
214+
}
215+
}
216+
});
217+
}
186218
});
187219
}
188220

@@ -191,6 +223,11 @@ public static boolean performAnalysis(String packageManager,
191223
} else {
192224
getInstance().vulnerabilityCache.invalidate(filePath);
193225
}
226+
if (!recommendationMap.isEmpty()) {
227+
getInstance().recommendationCache.put(filePath, recommendationMap);
228+
} else {
229+
getInstance().recommendationCache.invalidate(filePath);
230+
}
194231
getInstance().dependencyCache.put(filePath, dependencies);
195232
return true;
196233

src/main/java/org/jboss/tools/intellij/componentanalysis/CAUpdateManifestIntentionAction.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.intellij.util.IncorrectOperationException;
2222
import io.github.guacsec.trustifyda.api.PackageRef;
2323
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
24+
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
2425
import org.jetbrains.annotations.NotNull;
2526

2627
import java.util.Objects;
@@ -36,6 +37,16 @@ protected static String getRepositoryUrl(DependencyReport dependency) {
3637
return getRepositoryUrlFromPurl(dependency);
3738
}
3839

40+
/** Extracts the repository URL from a provider-level recommendation report. */
41+
protected static String getRepositoryUrl(RecommendationReport recReport) {
42+
if (recReport != null && recReport.getRecommendation() != null
43+
&& recReport.getRecommendation().purl() != null
44+
&& recReport.getRecommendation().purl().getQualifiers() != null) {
45+
return recReport.getRecommendation().purl().getQualifiers().get("repository_url");
46+
}
47+
return null;
48+
}
49+
3950
protected static String getRepositoryUrlFromPurl(DependencyReport dependency) {
4051
AtomicReference<PackageRef> packageRef = new AtomicReference<>();
4152
if(Objects.nonNull(dependency.getRecommendation()))
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Red Hat, Inc.
3+
* Distributed under license by Red Hat, Inc. All rights reserved.
4+
* This program is made available under the terms of the
5+
* Eclipse Public License v2.0 which accompanies this distribution,
6+
* and is available at http://www.eclipse.org/legal/epl-v20.html
7+
*
8+
* Contributors:
9+
* Red Hat, Inc. - initial API and implementation
10+
******************************************************************************/
11+
12+
package org.jboss.tools.intellij.componentanalysis;
13+
14+
import io.github.guacsec.trustifyda.api.PackageRef;
15+
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
16+
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
17+
import org.junit.Test;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertFalse;
21+
import static org.junit.Assert.assertNotNull;
22+
import static org.junit.Assert.assertTrue;
23+
24+
/**
25+
* Tests for provider-level recommendation support in CAIntentionAction.
26+
* Placed in the base package to access package-private methods.
27+
*/
28+
public class CAIntentionActionRecommendationTest {
29+
30+
private static final String NEW_VERSION = "4.0.0";
31+
32+
private RecommendationReport createProviderLevelRecommendation() {
33+
RecommendationReport recReport = new RecommendationReport();
34+
recReport.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
35+
recReport.setRecommendation(new PackageRef("pkg:maven/com.example/test@" + NEW_VERSION));
36+
return recReport;
37+
}
38+
39+
private DependencyReport createReportWithLegacyRecommendation() {
40+
DependencyReport report = new DependencyReport();
41+
report.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
42+
report.setRecommendation(new PackageRef("pkg:maven/com.example/test@" + NEW_VERSION));
43+
return report;
44+
}
45+
46+
private DependencyReport createReportWithoutRecommendation() {
47+
DependencyReport report = new DependencyReport();
48+
report.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
49+
return report;
50+
}
51+
52+
// ── thereIsRecommendation tests ────────────────────────────────────────────
53+
54+
/** Verifies that thereIsRecommendation detects a valid provider-level recommendation. */
55+
@Test
56+
public void testThereIsRecommendationWithProviderLevel() {
57+
RecommendationReport recReport = createProviderLevelRecommendation();
58+
assertTrue("Should detect provider-level recommendation",
59+
CAIntentionAction.thereIsRecommendation(recReport));
60+
}
61+
62+
/** Verifies that thereIsRecommendation returns false for null RecommendationReport. */
63+
@Test
64+
public void testThereIsRecommendationWithNull() {
65+
assertFalse("Should return false for null",
66+
CAIntentionAction.thereIsRecommendation((RecommendationReport) null));
67+
}
68+
69+
/** Verifies that thereIsRecommendation returns false for empty version. */
70+
@Test
71+
public void testThereIsRecommendationWithEmptyVersion() {
72+
RecommendationReport recReport = new RecommendationReport();
73+
recReport.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
74+
recReport.setRecommendation(new PackageRef("pkg:maven/com.example/test@"));
75+
assertFalse("Should return false for empty version",
76+
CAIntentionAction.thereIsRecommendation(recReport));
77+
}
78+
79+
/** Verifies that thereIsRecommendation still works with legacy DependencyReport path. */
80+
@Test
81+
public void testThereIsRecommendationWithLegacyReport() {
82+
DependencyReport report = createReportWithLegacyRecommendation();
83+
assertTrue("Should detect legacy recommendation",
84+
CAIntentionAction.thereIsRecommendation(report));
85+
}
86+
87+
/** Verifies that thereIsRecommendation returns false for DependencyReport without recommendation. */
88+
@Test
89+
public void testThereIsRecommendationFalseWithLegacyReport() {
90+
DependencyReport report = createReportWithoutRecommendation();
91+
assertFalse("Should return false when no recommendation set",
92+
CAIntentionAction.thereIsRecommendation(report));
93+
}
94+
95+
// ── isQuickFixAvailable tests ──────────────────────────────────────────────
96+
97+
/** Verifies that isQuickFixAvailable returns true for a valid provider-level recommendation. */
98+
@Test
99+
public void testIsQuickFixAvailableForProviderLevel() {
100+
RecommendationReport recReport = createProviderLevelRecommendation();
101+
assertTrue("Should be available for provider-level recommendation",
102+
CAIntentionAction.isQuickFixAvailable(recReport));
103+
}
104+
105+
/** Verifies that isQuickFixAvailable returns false for null RecommendationReport. */
106+
@Test
107+
public void testIsQuickFixAvailableForNull() {
108+
assertFalse("Should not be available for null",
109+
CAIntentionAction.isQuickFixAvailable((RecommendationReport) null));
110+
}
111+
112+
/** Verifies that isQuickFixAvailable returns true for legacy recommendation (no issues). */
113+
@Test
114+
public void testIsQuickFixAvailableForLegacyRecommendation() {
115+
DependencyReport report = createReportWithLegacyRecommendation();
116+
assertTrue("Should be available for legacy recommendation",
117+
CAIntentionAction.isQuickFixAvailable(report));
118+
}
119+
120+
// ── Version extraction tests ───────────────────────────────────────────────
121+
122+
/** Verifies that provider-level recommendation version is correctly extractable. */
123+
@Test
124+
public void testRecommendationVersionExtraction() {
125+
RecommendationReport recReport = createProviderLevelRecommendation();
126+
assertNotNull("Recommendation should not be null", recReport.getRecommendation());
127+
assertEquals("Version should match", NEW_VERSION, recReport.getRecommendation().version());
128+
}
129+
}

src/test/java/org/jboss/tools/intellij/componentanalysis/cargo/CargoCAIntentionActionTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public class CargoCAIntentionActionTest extends BasePlatformTestCase {
3333

3434
private DependencyReport createReportWithRecommendation() {
3535
DependencyReport report = new DependencyReport();
36+
report.setRef(new PackageRef("pkg:cargo/test-crate@1.0.0"));
3637
report.setRecommendation(new PackageRef("pkg:cargo/test-crate@" + CargoCAIntentionActionTest.NEW_VERSION));
3738
return report;
3839
}
@@ -376,4 +377,5 @@ public void testUpdateTargetSpecificComplexObjectDependency() {
376377
assertTrue("winapi features should remain unchanged",
377378
updatedText.contains("features = [\"winuser\"]"));
378379
}
380+
379381
}

0 commit comments

Comments
 (0)