Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import io.github.guacsec.trustifyda.ComponentAnalysisResult;
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.Source;
import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary;
import org.jboss.tools.intellij.exhort.ApiService;

import java.util.Collections;
Expand Down Expand Up @@ -56,12 +58,28 @@ public static CAService getInstance() {
.maximumSize(100)
.build();

private final Cache<String, LicenseSummary> licenseCache = Caffeine.newBuilder()
.maximumSize(100)
.build();

public static Map<Dependency, Map<VulnerabilitySource, DependencyReport>> getReports(String filePath) {
return Collections.unmodifiableMap(getInstance().vulnerabilityCache.get(filePath, p -> Collections.emptyMap()));
}

public static void deleteReports(String filePath) {
getInstance().vulnerabilityCache.invalidate(filePath);
getInstance().licenseCache.invalidate(filePath);
getInstance().dependencyCache.invalidate(filePath);
}

public static void invalidateAllCaches() {
getInstance().vulnerabilityCache.invalidateAll();
getInstance().licenseCache.invalidateAll();
getInstance().dependencyCache.invalidateAll();
}

public static LicenseSummary getLicenseSummary(String filePath) {
return getInstance().licenseCache.getIfPresent(filePath);
}

public static boolean dependenciesModified(String filePath, Set<Dependency> dependencies) {
Expand Down Expand Up @@ -90,10 +108,22 @@ public static boolean performAnalysis(String packageManager,
);
}

AnalysisReport report = apiService.getComponentAnalysis(packageManager, fileName, filePath);
if (report == null) {
ComponentAnalysisResult analysisResult = apiService.getComponentAnalysis(packageManager, fileName, filePath);
if (analysisResult == null) {
throw new RuntimeException("Failed to perform component analysis, result is invalid.");
}
AnalysisReport report = analysisResult.report();
if (report == null) {
throw new RuntimeException("Failed to perform component analysis, report is invalid.");
}

// Cache license summary
LicenseSummary licenseSummary = analysisResult.licenseSummary();
if (licenseSummary != null) {
getInstance().licenseCache.put(filePath, licenseSummary);
} else {
getInstance().licenseCache.invalidate(filePath);
}

Map<Dependency, Map<VulnerabilitySource, DependencyReport>> resultMap = new ConcurrentHashMap<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*******************************************************************************
* 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 com.intellij.codeInsight.intention.FileModifier;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.function.BiConsumer;

public class LicenseUpdateIntentionAction implements IntentionAction {

private final @FileModifier.SafeFieldForPreview PsiElement element;
private final String newLicense;
private final BiConsumer<PsiElement, String> replacer;

public LicenseUpdateIntentionAction(PsiElement element, String newLicense, BiConsumer<PsiElement, String> replacer) {
this.element = element;
this.newLicense = newLicense;
this.replacer = replacer;
}

@Override
public @IntentionName @NotNull String getText() {
return "Update manifest license to \"" + newLicense + "\" (from LICENSE file)";
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return "RHDA";
}

@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return element != null && element.isValid();
}

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
replacer.accept(element, newLicense);

// Invalidate caches so the next annotator pass triggers a fresh API call
if (file != null && file.getVirtualFile() != null) {
CAService.deleteReports(file.getVirtualFile().getPath());
}
}

@Override
public boolean startInWriteAction() {
return true;
}

@Override
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
PsiElement copy = PsiTreeUtil.findSameElementInCopy(this.element, target);
return new LicenseUpdateIntentionAction(copy, this.newLicense, this.replacer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
import org.jboss.tools.intellij.componentanalysis.Dependency;
import org.jboss.tools.intellij.componentanalysis.LicenseUpdateIntentionAction;
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
import org.jetbrains.annotations.Nullable;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.util.PsiTreeUtil;
Expand Down Expand Up @@ -320,6 +322,48 @@ private String extractVersionFromTomlValue(TomlValue tomlValue) {
return UNRESOLVED_VERSION;
}

@Override
protected @Nullable PsiElement getLicenseFieldPsiElement(PsiFile file) {
// Find [package] table, then "license" key-value
List<TomlTable> tables = PsiTreeUtil.getChildrenOfTypeAsList(file, TomlTable.class);
for (TomlTable table : tables) {
TomlTableHeader header = table.getHeader();
TomlKey key = header.getKey();
if (key == null) continue;
List<TomlKeySegment> segments = key.getSegments();
if (segments.size() == 1 && "package".equals(segments.get(0).getName())) {
List<TomlKeyValue> keyValues = PsiTreeUtil.getChildrenOfTypeAsList(table, TomlKeyValue.class);
for (TomlKeyValue kv : keyValues) {
if ("license".equals(normalizeKeyName(kv.getKey().getText()))) {
TomlValue value = kv.getValue();
if (value instanceof TomlLiteral) {
return value;
}
}
}
}
}
return null;
}

@Override
protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense) {
if (!(element instanceof TomlLiteral)) {
return null;
}
return new LicenseUpdateIntentionAction(element, newLicense, (el, license) -> {
// Replace the TOML literal text (including quotes), matching CargoCAIntentionAction pattern
Document doc = PsiDocumentManager.getInstance(el.getProject())
.getDocument(el.getContainingFile());
if (doc != null) {
int start = el.getTextRange().getStartOffset();
int end = el.getTextRange().getEndOffset();
doc.replaceString(start, end, "\"" + license + "\"");
PsiDocumentManager.getInstance(el.getProject()).commitDocument(doc);
}
});
}

private void parseFlatDependencies(TomlTable table, Set<String> ignoredDeps, Map<Dependency, List<PsiElement>> resultMap) {
// Parse inline format: [dependencies] with key-value pairs
// serde = "1.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
import org.jboss.tools.intellij.componentanalysis.Dependency;
import org.jboss.tools.intellij.componentanalysis.LicenseUpdateIntentionAction;
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
import org.jetbrains.annotations.Nullable;

import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -142,6 +144,17 @@ protected boolean isQuickFixApplicable(PsiElement element) {
return element != null && element.getContainingFile().getName().equals("go.mod");
}

// go.mod has no license field; license detection relies on the LICENSE file
@Override
protected @Nullable PsiElement getLicenseFieldPsiElement(PsiFile file) {
return null;
}

@Override
protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense) {
return null;
}

private PsiElement findElementAtLine(PsiFile file, int lineNumber) {
String[] lines = file.getText().split("\\n");
if (lineNumber >= lines.length) return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
import org.jboss.tools.intellij.componentanalysis.Dependency;
import org.jboss.tools.intellij.componentanalysis.LicenseUpdateIntentionAction;
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
import org.jetbrains.annotations.Nullable;
import org.jboss.tools.intellij.componentanalysis.gradle.build.psi.Artifact;

import java.util.Arrays;
Expand Down Expand Up @@ -75,4 +77,15 @@ protected CAUpdateManifestIntentionAction patchManifest(PsiElement element, Depe
protected boolean isQuickFixApplicable(PsiElement element) {
return true;
}

// Gradle has no standardized license field location; license detection relies on the LICENSE file
@Override
protected @Nullable PsiElement getLicenseFieldPsiElement(PsiFile file) {
return null;
}

@Override
protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
import org.jboss.tools.intellij.componentanalysis.Dependency;
import org.jboss.tools.intellij.componentanalysis.LicenseUpdateIntentionAction;
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
import org.jetbrains.annotations.Nullable;


import java.util.Arrays;
Expand Down Expand Up @@ -146,4 +148,32 @@ protected boolean isQuickFixApplicable(PsiElement element) {
}
return false;
}

@Override
protected @Nullable PsiElement getLicenseFieldPsiElement(PsiFile file) {
// Find <project><licenses><license><name> text element
return Arrays.stream(file.getChildren())
.filter(e -> e instanceof XmlDocument)
.flatMap(e -> Arrays.stream(e.getChildren()))
.filter(e -> e instanceof XmlTag && "project".equals(((XmlTag) e).getName()))
.flatMap(e -> Arrays.stream(e.getChildren()))
.filter(e -> e instanceof XmlTag && "licenses".equals(((XmlTag) e).getName()))
.flatMap(e -> Arrays.stream(e.getChildren()))
.filter(e -> e instanceof XmlTag && "license".equals(((XmlTag) e).getName()))
.flatMap(e -> Arrays.stream(e.getChildren()))
.filter(e -> e instanceof XmlTag && "name".equals(((XmlTag) e).getName()))
.flatMap(e -> Arrays.stream(((XmlTag) e).getValue().getChildren()))
.filter(e -> e instanceof XmlText)
.findFirst()
.orElse(null);
}

@Override
protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense) {
if (!(element instanceof XmlText)) {
return null;
}
return new LicenseUpdateIntentionAction(element, newLicense, (el, license) ->
((XmlText) el).setValue(license));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

package org.jboss.tools.intellij.componentanalysis.npm;

import com.intellij.json.psi.JsonElementGenerator;
import com.intellij.json.psi.JsonObject;
import com.intellij.json.psi.JsonProperty;
import com.intellij.json.psi.JsonStringLiteral;
import com.intellij.psi.PsiElement;
Expand All @@ -20,8 +22,11 @@
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
import org.jboss.tools.intellij.componentanalysis.Dependency;
import org.jboss.tools.intellij.componentanalysis.LicenseUpdateIntentionAction;
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -53,4 +58,27 @@ protected CAUpdateManifestIntentionAction patchManifest(PsiElement element, Depe
protected boolean isQuickFixApplicable(PsiElement element) {
return element instanceof JsonProperty && ((JsonProperty) element).getValue() instanceof JsonStringLiteral;
}

@Override
protected @Nullable PsiElement getLicenseFieldPsiElement(PsiFile file) {
return Arrays.stream(file.getChildren())
.filter(e -> e instanceof JsonObject)
.flatMap(e -> Arrays.stream(e.getChildren()))
.filter(e -> e instanceof JsonProperty && "license".equals(((JsonProperty) e).getName()))
.map(e -> ((JsonProperty) e).getValue())
.filter(v -> v instanceof JsonStringLiteral)
.findFirst()
.orElse(null);
}

@Override
protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense) {
if (!(element instanceof JsonStringLiteral)) {
return null;
}
return new LicenseUpdateIntentionAction(element, newLicense, (el, license) -> {
JsonStringLiteral newValue = new JsonElementGenerator(el.getProject()).createStringLiteral(license);
el.replace(newValue);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import org.jboss.tools.intellij.componentanalysis.CAIntentionAction;
import org.jboss.tools.intellij.componentanalysis.CAUpdateManifestIntentionAction;
import org.jboss.tools.intellij.componentanalysis.Dependency;
import org.jboss.tools.intellij.componentanalysis.LicenseUpdateIntentionAction;
import org.jboss.tools.intellij.componentanalysis.VulnerabilitySource;
import org.jetbrains.annotations.Nullable;
import org.jboss.tools.intellij.componentanalysis.pypi.requirements.psi.NameReq;
import org.jboss.tools.intellij.componentanalysis.pypi.requirements.psi.NameReqComment;

Expand Down Expand Up @@ -82,4 +84,15 @@ protected CAUpdateManifestIntentionAction patchManifest(PsiElement element, Depe
protected boolean isQuickFixApplicable(PsiElement element) {
return element instanceof NameReq && ((NameReq) element).getVersionspec() != null;
}

// requirements.txt has no license field; license detection relies on the LICENSE file
@Override
protected @Nullable PsiElement getLicenseFieldPsiElement(PsiFile file) {
return null;
}

@Override
protected @Nullable LicenseUpdateIntentionAction createLicenseUpdateFix(PsiElement element, String newLicense) {
return null;
}
}
Loading
Loading