Skip to content

Commit 8fb9123

Browse files
committed
feat: Add support for user-configurable patterns/globs for excluding manifests from Component Analysis
Signed-off-by: Chao Wang <[email protected]>
1 parent fbfa21a commit 8fb9123

File tree

9 files changed

+490
-1
lines changed

9 files changed

+490
-1
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ dependencies {
8888

8989
// for tests
9090
testImplementation(libs.junit)
91+
testImplementation(libs.mockito)
9192
}
9293

9394
tasks {

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ exhort-api-spec = "1.0.18"
77
exhort-java-api = "0.0.8"
88
github-api = "1.314"
99
junit = "4.13.2"
10+
mockito = "4.11.0"
1011
packageurl-java = "1.4.1"
1112

1213
# plugins
@@ -20,6 +21,7 @@ exhort-api-spec = { group = "com.redhat.ecosystemappeng", name = "exhort-api-spe
2021
exhort-java-api = { group = "com.redhat.exhort", name = "exhort-java-api", version.ref = "exhort-java-api" }
2122
github-api = { group = "org.kohsuke", name = "github-api", version.ref = "github-api" }
2223
junit = { group = "junit", name = "junit", version.ref = "junit" }
24+
mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" }
2325
packageurl-java = { group = "com.github.package-url", name = "packageurl-java", version.ref = "packageurl-java" }
2426

2527
[plugins]

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public abstract class CAAnnotator extends ExternalAnnotator<CAAnnotator.Info, Ma
5252
if (inspection == null) {
5353
return null;
5454
}
55+
56+
if (ManifestExclusionManager.isManifestExcluded(file.getVirtualFile(), file.getProject())) {
57+
LOG.debug("Skipping analysis for excluded manifest: " + file.getName());
58+
return null;
59+
}
60+
5561
LOG.info("Get dependencies");
5662
return new Info(file, this.getDependencies(file));
5763
}
@@ -177,6 +183,7 @@ public void apply(@NotNull PsiFile file, Map<Dependency, Result> annotationResul
177183
}
178184
}
179185
builder.withFix(new SAIntentionAction());
186+
builder.withFix(new ExcludeManifestIntentionAction());
180187
builder.create();
181188
}
182189
);
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
15+
import com.intellij.codeInsight.intention.FileModifier;
16+
import com.intellij.codeInsight.intention.IntentionAction;
17+
import com.intellij.codeInspection.util.IntentionFamilyName;
18+
import com.intellij.codeInspection.util.IntentionName;
19+
import com.intellij.openapi.application.ApplicationManager;
20+
import com.intellij.openapi.editor.Editor;
21+
import com.intellij.openapi.project.Project;
22+
import com.intellij.openapi.vfs.VirtualFile;
23+
import com.intellij.psi.PsiFile;
24+
import com.intellij.util.IncorrectOperationException;
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
27+
28+
public class ExcludeManifestIntentionAction implements IntentionAction {
29+
30+
@Override
31+
public @IntentionName @NotNull String getText() {
32+
return "Exclude this manifest from component analysis";
33+
}
34+
35+
@Override
36+
public @NotNull @IntentionFamilyName String getFamilyName() {
37+
return "RHDA";
38+
}
39+
40+
@Override
41+
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
42+
if (file == null || file.getVirtualFile() == null) {
43+
return false;
44+
}
45+
46+
String fileName = file.getName();
47+
return "pom.xml".equals(fileName) ||
48+
"package.json".equals(fileName) ||
49+
"go.mod".equals(fileName) ||
50+
"requirements.txt".equals(fileName) ||
51+
"build.gradle".equals(fileName);
52+
}
53+
54+
@Override
55+
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
56+
VirtualFile virtualFile = file.getVirtualFile();
57+
if (virtualFile == null) {
58+
return;
59+
}
60+
61+
VirtualFile projectRoot = project.getBaseDir();
62+
if (projectRoot == null) {
63+
return;
64+
}
65+
66+
String filePath = virtualFile.getPath();
67+
String projectPath = projectRoot.getPath();
68+
69+
if (filePath.startsWith(projectPath)) {
70+
String relativePath = filePath.substring(projectPath.length());
71+
if (relativePath.startsWith("/") || relativePath.startsWith("\\")) {
72+
relativePath = relativePath.substring(1);
73+
}
74+
75+
ManifestExclusionManager.addExclusionPattern(relativePath, project);
76+
77+
ApplicationManager.getApplication().runReadAction(() -> {
78+
DaemonCodeAnalyzer.getInstance(project).restart(file);
79+
});
80+
}
81+
}
82+
83+
@Override
84+
public boolean startInWriteAction() {
85+
return false;
86+
}
87+
88+
@Override
89+
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
90+
return null;
91+
}
92+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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 com.intellij.notification.Notification;
15+
import com.intellij.notification.NotificationType;
16+
import com.intellij.notification.Notifications;
17+
import com.intellij.openapi.diagnostic.Logger;
18+
import com.intellij.openapi.project.Project;
19+
import com.intellij.openapi.vfs.VirtualFile;
20+
import org.jboss.tools.intellij.settings.ApiSettingsState;
21+
22+
import java.nio.file.FileSystems;
23+
import java.nio.file.Path;
24+
import java.nio.file.PathMatcher;
25+
import java.nio.file.Paths;
26+
import java.util.Arrays;
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.stream.Collectors;
30+
31+
public class ManifestExclusionManager {
32+
33+
private static final Logger LOG = Logger.getInstance(ManifestExclusionManager.class);
34+
35+
public static boolean isManifestExcluded(VirtualFile file, Project project) {
36+
if (file == null) {
37+
return false;
38+
}
39+
40+
ApiSettingsState settings = ApiSettingsState.getInstance();
41+
String patterns = settings.manifestExclusionPatterns;
42+
43+
if (patterns == null || patterns.trim().isEmpty()) {
44+
return false;
45+
}
46+
47+
VirtualFile projectRoot = project.getBaseDir();
48+
if (projectRoot == null) {
49+
return false;
50+
}
51+
52+
String relativePath = getRelativePath(file, projectRoot);
53+
if (relativePath == null) {
54+
return false;
55+
}
56+
57+
List<String> exclusionPatterns = parsePatterns(patterns);
58+
return matchesAnyPattern(relativePath, exclusionPatterns);
59+
}
60+
61+
public static void addExclusionPattern(String manifestPath, Project project) {
62+
if (manifestPath == null || manifestPath.trim().isEmpty()) {
63+
return;
64+
}
65+
66+
VirtualFile projectRoot = project.getBaseDir();
67+
if (projectRoot == null) {
68+
return;
69+
}
70+
71+
VirtualFile manifestFile = project.getBaseDir().findFileByRelativePath(manifestPath);
72+
if (manifestFile == null) {
73+
return;
74+
}
75+
76+
String relativePath = getRelativePath(manifestFile, projectRoot);
77+
if (relativePath == null) {
78+
return;
79+
}
80+
81+
ApiSettingsState settings = ApiSettingsState.getInstance();
82+
List<String> currentPatterns = parsePatterns(settings.manifestExclusionPatterns);
83+
84+
if (!currentPatterns.contains(relativePath)) {
85+
currentPatterns.add(relativePath);
86+
settings.manifestExclusionPatterns = String.join("\n", currentPatterns);
87+
}
88+
}
89+
90+
private static String getRelativePath(VirtualFile file, VirtualFile projectRoot) {
91+
try {
92+
String filePath = file.getPath();
93+
String projectPath = projectRoot.getPath();
94+
95+
if (filePath.startsWith(projectPath)) {
96+
String relativePath = filePath.substring(projectPath.length());
97+
if (relativePath.startsWith("/") || relativePath.startsWith("\\")) {
98+
relativePath = relativePath.substring(1);
99+
}
100+
return relativePath;
101+
}
102+
} catch (Exception e) {
103+
LOG.warn("Failed to get relative path for file: " + file.getPath(), e);
104+
}
105+
return null;
106+
}
107+
108+
private static List<String> parsePatterns(String patterns) {
109+
if (patterns == null || patterns.trim().isEmpty()) {
110+
return Collections.emptyList();
111+
}
112+
113+
return Arrays.stream(patterns.split("[\n\r]+"))
114+
.map(String::trim)
115+
.filter(pattern -> !pattern.isEmpty() && !pattern.startsWith("#"))
116+
.collect(Collectors.toList());
117+
}
118+
119+
private static boolean matchesAnyPattern(String path, List<String> patterns) {
120+
Path filePath = Paths.get(path);
121+
122+
for (String pattern : patterns) {
123+
try {
124+
// Test the pattern as-is first
125+
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern);
126+
if (matcher.matches(filePath)) {
127+
LOG.debug("File " + path + " matches exclusion pattern: " + pattern);
128+
return true;
129+
}
130+
131+
// For patterns starting with "**/" that don't match, also test against
132+
// the path with a virtual directory prefix to handle the Java PathMatcher
133+
// limitation where "**/file.txt" doesn't match "file.txt" at root
134+
if (pattern.startsWith("**/")) {
135+
// Try matching with a virtual directory prefix
136+
Path prefixedPath = Paths.get("dummy/" + path);
137+
if (matcher.matches(prefixedPath)) {
138+
LOG.debug("File " + path + " matches exclusion pattern: " + pattern + " (with directory prefix)");
139+
return true;
140+
}
141+
}
142+
} catch (Exception e) {
143+
LOG.warn("Invalid glob pattern: " + pattern, e);
144+
}
145+
}
146+
147+
return false;
148+
}
149+
150+
public static List<String> getExclusionPatterns() {
151+
ApiSettingsState settings = ApiSettingsState.getInstance();
152+
return parsePatterns(settings.manifestExclusionPatterns);
153+
}
154+
155+
public static void setExclusionPatterns(List<String> patterns) {
156+
ApiSettingsState settings = ApiSettingsState.getInstance();
157+
settings.manifestExclusionPatterns = String.join("\n", patterns);
158+
}
159+
}

src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import com.intellij.ui.components.JBCheckBox;
1919
import com.intellij.ui.components.JBLabel;
2020
import com.intellij.ui.components.JBTextField;
21+
import com.intellij.ui.components.JBTextArea;
22+
import com.intellij.ui.components.JBScrollPane;
2123
import com.intellij.util.ui.FormBuilder;
2224
import org.jetbrains.annotations.NotNull;
2325

@@ -74,6 +76,9 @@ public class ApiSettingsComponent {
7476
+ "<br>Specifies absolute path of <b>podman</b> executable.</html>";
7577
private final static String imagePlatformLabel = "<html>Image > Build: <b>Platform</b>"
7678
+ "<br>Specifies the platform of the images, e.g. <b>linux/amd64</b> or <b>linux/arm64</b>.</html>";
79+
private final static String manifestExclusionPatternsLabel = "<html>Component Analysis > Exclusion Patterns"
80+
+ "<br>Specifies glob patterns for manifest files to exclude from component analysis."
81+
+ "<br>One pattern per line. Examples: <b>**/node_modules/**/package.json</b>, <b>test/**/pom.xml</b></html>";
7782

7883
private final JPanel mainPanel;
7984

@@ -102,6 +107,8 @@ public class ApiSettingsComponent {
102107
private final TextFieldWithBrowseButton dockerPathText;
103108
private final TextFieldWithBrowseButton podmanPathText;
104109
private final JBTextField imagePlatformText;
110+
private final JBTextArea manifestExclusionPatternsText;
111+
private final JBScrollPane manifestExclusionPatternsScrollPane;
105112

106113

107114
public ApiSettingsComponent() {
@@ -232,6 +239,11 @@ public ApiSettingsComponent() {
232239

233240
imagePlatformText = new JBTextField();
234241

242+
manifestExclusionPatternsText = new JBTextArea();
243+
manifestExclusionPatternsText.setRows(5);
244+
manifestExclusionPatternsText.setColumns(50);
245+
manifestExclusionPatternsScrollPane = new JBScrollPane(manifestExclusionPatternsText);
246+
235247
mainPanel = FormBuilder.createFormBuilder()
236248
.addLabeledComponent(new JBLabel(mvnPathLabel), mvnPathText, 1, true)
237249
.addVerticalGap(10)
@@ -283,6 +295,9 @@ public ApiSettingsComponent() {
283295
.addLabeledComponent(new JBLabel(podmanPathLabel), podmanPathText, 1, true)
284296
.addVerticalGap(10)
285297
.addLabeledComponent(new JBLabel(imagePlatformLabel), imagePlatformText, 1, true)
298+
.addSeparator(10)
299+
.addVerticalGap(10)
300+
.addLabeledComponent(new JBLabel(manifestExclusionPatternsLabel), manifestExclusionPatternsScrollPane, 1, true)
286301
.addComponentFillVertically(new JPanel(), 0)
287302
.getPanel();
288303
}
@@ -495,4 +510,13 @@ public String getGradlePathText() {
495510
public void setGradlePathText(@NotNull String text) {
496511
gradlePathText.setText(text);
497512
}
513+
514+
@NotNull
515+
public String getManifestExclusionPatternsText() {
516+
return manifestExclusionPatternsText.getText();
517+
}
518+
519+
public void setManifestExclusionPatternsText(@NotNull String text) {
520+
manifestExclusionPatternsText.setText(text);
521+
}
498522
}

0 commit comments

Comments
 (0)