diff --git a/README.md b/README.md index 2112a94..e9a755e 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,10 @@ according to your preferences. - **Proxy Configuration** :
From IntelliJ IDEA Appearance & Behavior > System Settings > HTTP Proxy, you can configure a static proxy for all HTTP requests made by the plugin. This is useful when your environment requires going through a proxy to access external services. For example:`http://proxy.example.com:8080` +- **Manifest Exclusion Patterns** : +
You can exclude manifest files from component analysis using glob patterns. This is useful for excluding third-party dependencies, test files, or other manifests that should not be analyzed. +
Enter one pattern per line. Examples: `**/node_modules/**/package.json` to exclude all package.json files in node_modules directories, or `test/**/pom.xml` to exclude all Maven files in test directories. + ## Features - **Component analysis** @@ -294,6 +298,18 @@ according to your preferences. You can create an alternative file to `requirements.txt`, for example, a `requirements-dev.txt` or a `requirements-test.txt` file where you can add the development or test dependencies there. + +- **Excluding manifest files with patterns** +
You can exclude specific manifest files from component analysis using configurable glob patterns. This feature allows you to avoid analyzing third-party dependencies, test files, or other manifests that are not relevant to your security analysis. +
Patterns are configured in the plugin settings under **Tools > Red Hat Dependency Analytics > Manifest Exclusion Patterns**. +
Examples of exclusion patterns: + - `**/node_modules/**/package.json` - Excludes all package.json files in node_modules directories + - `test/**/pom.xml` - Excludes all Maven pom.xml files in test directories + - `vendor/**/*.go.mod` - Excludes all go.mod files in vendor directories + - `**/build.gradle` - Excludes all Gradle build files +
Right-click on any manifest file and select **Exclude from Component Analysis** to quickly add an exclusion pattern for that specific file. + + - **Red Hat Dependency Analytics report**
The Red Hat Dependency Analytics report is a temporary HTML file that exist if the **Red Hat Dependency Analytics Report** tab remains open. diff --git a/build.gradle.kts b/build.gradle.kts index e574f69..c4d8332 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { // for tests testImplementation(libs.junit) + testImplementation(libs.mockito) } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 72c5694..d8a4e72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ exhort-api-spec = "1.0.18" exhort-java-api = "0.0.8" github-api = "1.314" junit = "4.13.2" +mockito = "4.11.0" packageurl-java = "1.4.1" # plugins @@ -20,6 +21,7 @@ exhort-api-spec = { group = "com.redhat.ecosystemappeng", name = "exhort-api-spe exhort-java-api = { group = "com.redhat.exhort", name = "exhort-java-api", version.ref = "exhort-java-api" } github-api = { group = "org.kohsuke", name = "github-api", version.ref = "github-api" } junit = { group = "junit", name = "junit", version.ref = "junit" } +mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } packageurl-java = { group = "com.github.package-url", name = "packageurl-java", version.ref = "packageurl-java" } [plugins] 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 091d55e..2eb844d 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAAnnotator.java @@ -52,6 +52,12 @@ public abstract class CAAnnotator extends ExternalAnnotator annotationResul } } builder.withFix(new SAIntentionAction()); + builder.withFix(new ExcludeManifestIntentionAction()); builder.create(); } ); 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 b666430..5b5d3c5 100644 --- a/src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/CAService.java @@ -27,12 +27,8 @@ import com.redhat.exhort.api.v4.DependencyReport; import com.redhat.exhort.api.v4.ProviderReport; import com.redhat.exhort.api.v4.Source; -import org.apache.commons.io.FileUtils; import org.jboss.tools.intellij.exhort.ApiService; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -171,14 +167,4 @@ public static boolean performAnalysis(String packageManager, } return false; } - - private static void deleteTempDir(Path tempDirectory) { - try { - FileUtils.deleteDirectory(tempDirectory.toFile()); - } catch (IOException e) { - LOG.warn("Failed to delete temp directory: " + tempDirectory, e); - } - } - - } diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/ExcludeManifestIntentionAction.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/ExcludeManifestIntentionAction.java new file mode 100644 index 0000000..bb2ff05 --- /dev/null +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/ExcludeManifestIntentionAction.java @@ -0,0 +1,92 @@ +/******************************************************************************* + * 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.daemon.DaemonCodeAnalyzer; +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.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ExcludeManifestIntentionAction implements IntentionAction { + + @Override + public @IntentionName @NotNull String getText() { + return "Exclude this manifest from component analysis"; + } + + @Override + public @NotNull @IntentionFamilyName String getFamilyName() { + return "RHDA"; + } + + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) { + if (file == null || file.getVirtualFile() == null) { + return false; + } + + String fileName = file.getName(); + return "pom.xml".equals(fileName) || + "package.json".equals(fileName) || + "go.mod".equals(fileName) || + "requirements.txt".equals(fileName) || + "build.gradle".equals(fileName); + } + + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException { + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile == null) { + return; + } + + VirtualFile projectRoot = project.getBaseDir(); + if (projectRoot == null) { + return; + } + + String filePath = virtualFile.getPath(); + String projectPath = projectRoot.getPath(); + + if (filePath.startsWith(projectPath)) { + String relativePath = filePath.substring(projectPath.length()); + if (relativePath.startsWith("/") || relativePath.startsWith("\\")) { + relativePath = relativePath.substring(1); + } + + ManifestExclusionManager.addExclusionPattern(relativePath, project); + + ApplicationManager.getApplication().runReadAction(() -> { + DaemonCodeAnalyzer.getInstance(project).restart(file); + }); + } + } + + @Override + public boolean startInWriteAction() { + return false; + } + + @Override + public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) { + return null; + } +} \ No newline at end of file diff --git a/src/main/java/org/jboss/tools/intellij/componentanalysis/ManifestExclusionManager.java b/src/main/java/org/jboss/tools/intellij/componentanalysis/ManifestExclusionManager.java new file mode 100644 index 0000000..c4a0dcf --- /dev/null +++ b/src/main/java/org/jboss/tools/intellij/componentanalysis/ManifestExclusionManager.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * 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.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jboss.tools.intellij.settings.ApiSettingsState; + +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class ManifestExclusionManager { + + private static final Logger LOG = Logger.getInstance(ManifestExclusionManager.class); + + public static boolean isManifestExcluded(VirtualFile file, Project project) { + if (file == null) { + return false; + } + + ApiSettingsState settings = ApiSettingsState.getInstance(); + String patterns = settings.manifestExclusionPatterns; + + if (patterns == null || patterns.trim().isEmpty()) { + return false; + } + + VirtualFile projectRoot = project.getBaseDir(); + if (projectRoot == null) { + return false; + } + + String relativePath = getRelativePath(file, projectRoot); + if (relativePath == null) { + return false; + } + + List exclusionPatterns = parsePatterns(patterns); + return matchesAnyPattern(relativePath, exclusionPatterns); + } + + public static void addExclusionPattern(String manifestPath, Project project) { + if (manifestPath == null || manifestPath.trim().isEmpty()) { + return; + } + + VirtualFile projectRoot = project.getBaseDir(); + if (projectRoot == null) { + return; + } + + VirtualFile manifestFile = project.getBaseDir().findFileByRelativePath(manifestPath); + if (manifestFile == null) { + return; + } + + String relativePath = getRelativePath(manifestFile, projectRoot); + if (relativePath == null) { + return; + } + + ApiSettingsState settings = ApiSettingsState.getInstance(); + List currentPatterns = parsePatterns(settings.manifestExclusionPatterns); + + if (!currentPatterns.contains(relativePath)) { + currentPatterns.add(relativePath); + settings.manifestExclusionPatterns = String.join("\n", currentPatterns); + } + } + + private static String getRelativePath(VirtualFile file, VirtualFile projectRoot) { + try { + String filePath = file.getPath(); + String projectPath = projectRoot.getPath(); + + if (filePath.startsWith(projectPath)) { + String relativePath = filePath.substring(projectPath.length()); + if (relativePath.startsWith("/") || relativePath.startsWith("\\")) { + relativePath = relativePath.substring(1); + } + return relativePath; + } + } catch (Exception e) { + LOG.warn("Failed to get relative path for file: " + file.getPath(), e); + } + return null; + } + + private static List parsePatterns(String patterns) { + if (patterns == null || patterns.trim().isEmpty()) { + return Collections.emptyList(); + } + + return Arrays.stream(patterns.split("[\n\r]+")) + .map(String::trim) + .filter(pattern -> !pattern.isEmpty() && !pattern.startsWith("#")) + .collect(Collectors.toList()); + } + + private static boolean matchesAnyPattern(String path, List patterns) { + Path filePath = Paths.get(path); + + for (String pattern : patterns) { + try { + // Test the pattern as-is first + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + pattern); + if (matcher.matches(filePath)) { + LOG.debug("File " + path + " matches exclusion pattern: " + pattern); + return true; + } + + // For patterns starting with "**/" that don't match, also test against + // the path with a virtual directory prefix to handle the Java PathMatcher + // limitation where "**/file.txt" doesn't match "file.txt" at root + if (pattern.startsWith("**/")) { + // Try matching with a virtual directory prefix + Path prefixedPath = Paths.get("dummy/" + path); + if (matcher.matches(prefixedPath)) { + LOG.debug("File " + path + " matches exclusion pattern: " + pattern + " (with directory prefix)"); + return true; + } + } + } catch (Exception e) { + LOG.warn("Invalid glob pattern: " + pattern, e); + } + } + + return false; + } + + public static List getExclusionPatterns() { + ApiSettingsState settings = ApiSettingsState.getInstance(); + return parsePatterns(settings.manifestExclusionPatterns); + } + + public static void setExclusionPatterns(List patterns) { + ApiSettingsState settings = ApiSettingsState.getInstance(); + settings.manifestExclusionPatterns = String.join("\n", patterns); + } +} \ No newline at end of file diff --git a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java index 2fbc7b4..f4f4122 100644 --- a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java +++ b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsComponent.java @@ -18,6 +18,8 @@ import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBTextField; +import com.intellij.ui.components.JBTextArea; +import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ui.FormBuilder; import org.jetbrains.annotations.NotNull; @@ -74,6 +76,9 @@ public class ApiSettingsComponent { + "
Specifies absolute path of podman executable."; private final static String imagePlatformLabel = "Image > Build: Platform" + "
Specifies the platform of the images, e.g. linux/amd64 or linux/arm64."; + private final static String manifestExclusionPatternsLabel = "Component Analysis > Exclusion Patterns" + + "
Specifies glob patterns for manifest files to exclude from component analysis." + + "
One pattern per line. Examples: **/node_modules/**/package.json, test/**/pom.xml"; private final JPanel mainPanel; @@ -102,6 +107,8 @@ public class ApiSettingsComponent { private final TextFieldWithBrowseButton dockerPathText; private final TextFieldWithBrowseButton podmanPathText; private final JBTextField imagePlatformText; + private final JBTextArea manifestExclusionPatternsText; + private final JBScrollPane manifestExclusionPatternsScrollPane; public ApiSettingsComponent() { @@ -232,6 +239,11 @@ public ApiSettingsComponent() { imagePlatformText = new JBTextField(); + manifestExclusionPatternsText = new JBTextArea(); + manifestExclusionPatternsText.setRows(5); + manifestExclusionPatternsText.setColumns(50); + manifestExclusionPatternsScrollPane = new JBScrollPane(manifestExclusionPatternsText); + mainPanel = FormBuilder.createFormBuilder() .addLabeledComponent(new JBLabel(mvnPathLabel), mvnPathText, 1, true) .addVerticalGap(10) @@ -283,6 +295,9 @@ public ApiSettingsComponent() { .addLabeledComponent(new JBLabel(podmanPathLabel), podmanPathText, 1, true) .addVerticalGap(10) .addLabeledComponent(new JBLabel(imagePlatformLabel), imagePlatformText, 1, true) + .addSeparator(10) + .addVerticalGap(10) + .addLabeledComponent(new JBLabel(manifestExclusionPatternsLabel), manifestExclusionPatternsScrollPane, 1, true) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } @@ -495,4 +510,13 @@ public String getGradlePathText() { public void setGradlePathText(@NotNull String text) { gradlePathText.setText(text); } + + @NotNull + public String getManifestExclusionPatternsText() { + return manifestExclusionPatternsText.getText(); + } + + public void setManifestExclusionPatternsText(@NotNull String text) { + manifestExclusionPatternsText.setText(text); + } } diff --git a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java index 7accd43..5736485 100644 --- a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java +++ b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsConfigurable.java @@ -11,10 +11,18 @@ package org.jboss.tools.intellij.settings; +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.NlsContexts; import org.jetbrains.annotations.Nullable; import javax.swing.JComponent; +import java.util.Objects; public class ApiSettingsConfigurable implements com.intellij.openapi.options.Configurable { @@ -40,7 +48,7 @@ public JComponent getPreferredFocusedComponent() { public boolean isModified() { ApiSettingsState settings = ApiSettingsState.getInstance(); boolean modified = !settingsComponent.getMvnPathText().equals(settings.mvnPath); - modified |= settingsComponent.getUseMavenWrapperCombo() != settings.useMavenWrapper; + modified |= !Objects.equals(settingsComponent.getUseMavenWrapperCombo(), settings.useMavenWrapper); modified |= !settingsComponent.getJavaPathText().equals(settings.javaPath); modified |= !settingsComponent.getNpmPathText().equals(settings.npmPath); modified |= !settingsComponent.getPnpmPathText().equals(settings.pnpmPath); @@ -62,12 +70,14 @@ public boolean isModified() { modified |= !settingsComponent.getPodmanPathText().equals(settings.podmanPath); modified |= !settingsComponent.getImagePlatformText().equals(settings.imagePlatform); modified |= !settingsComponent.getGradlePathText().equals(settings.gradlePath); + modified |= !settingsComponent.getManifestExclusionPatternsText().equals(settings.manifestExclusionPatterns); return modified; } @Override public void apply() { ApiSettingsState settings = ApiSettingsState.getInstance(); + settings.mvnPath = settingsComponent.getMvnPathText(); settings.useMavenWrapper = settingsComponent.getUseMavenWrapperCombo(); settings.javaPath = settingsComponent.getJavaPathText(); @@ -91,6 +101,28 @@ public void apply() { settings.podmanPath = settingsComponent.getPodmanPathText(); settings.imagePlatform = settingsComponent.getImagePlatformText(); settings.gradlePath = settingsComponent.getGradlePathText(); + + // Check if exclusion patterns changed + String oldPatterns = settings.manifestExclusionPatterns; + String newPatterns = settingsComponent.getManifestExclusionPatternsText(); + boolean patternsChanged = !Objects.equals(oldPatterns, newPatterns); + settings.manifestExclusionPatterns = newPatterns; + + // Trigger re-analysis if exclusion patterns changed + if (patternsChanged) { + refreshComponentAnalysis(); + } + } + + private void refreshComponentAnalysis() { + ApplicationManager.getApplication().runReadAction(() -> { + Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); + for (Project project : openProjects) { + if (!project.isDisposed()) { + DaemonCodeAnalyzer.getInstance(project).restart(); + } + } + }); } @Override @@ -119,6 +151,7 @@ public void reset() { settingsComponent.setPodmanPathText(settings.podmanPath); settingsComponent.setImagePlatformText(settings.imagePlatform); settingsComponent.setGradlePathText(settings.gradlePath != null ? settings.gradlePath : ""); + settingsComponent.setManifestExclusionPatternsText(settings.manifestExclusionPatterns != null ? settings.manifestExclusionPatterns : ""); } @Override diff --git a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java index 193c2c0..33d772e 100644 --- a/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java +++ b/src/main/java/org/jboss/tools/intellij/settings/ApiSettingsState.java @@ -63,6 +63,8 @@ public final class ApiSettingsState implements PersistentStateComponentYou can set the vulnerability severity alert level to Error or Warning for inline notifications of detected vulnerabilities. +
  • + Manifest Exclusion Patterns: +
    You can exclude manifest files from component analysis using glob patterns. This is useful for excluding + third-party dependencies, test files, or other manifests that should not be analyzed. +
    Enter one pattern per line. Examples: **/node_modules/**/package.json to exclude all + package.json files in node_modules directories, or test/**/pom.xml to exclude all Maven files + in test directories. +
  • @@ -344,6 +352,18 @@ Analytics Report tab remains open.
    Closing the tab removes the temporary HTML file. +

  • + Excluding manifest files with patterns +
    You can exclude specific manifest files from component analysis using configurable glob patterns. This + feature allows you to avoid analyzing third-party dependencies, test files, or other manifests that are not + relevant to your security analysis. +
    Patterns are configured in the plugin settings under Tools > Red Hat Dependency Analytics > Manifest + Exclusion Patterns. +
    Examples: **/node_modules/**/package.json, test/**/pom.xml, + vendor/**/*.go.mod +
    Right-click on any manifest file and select Exclude from Component Analysis to quickly add an + exclusion pattern for that specific file. +
  • @@ -390,6 +410,8 @@ ]]> 1.2.0

    +

    Add support for user-configurable patterns/globs for excluding manifests from Component Analysis.

    1.1.0

    Added support for Gradle manifest files.

    Added support for Yarn.

    diff --git a/src/test/java/org/jboss/tools/intellij/componentanalysis/ManifestExclusionManagerSimpleTest.java b/src/test/java/org/jboss/tools/intellij/componentanalysis/ManifestExclusionManagerSimpleTest.java new file mode 100644 index 0000000..0eabc0f --- /dev/null +++ b/src/test/java/org/jboss/tools/intellij/componentanalysis/ManifestExclusionManagerSimpleTest.java @@ -0,0 +1,169 @@ +/******************************************************************************* + * 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 org.junit.Test; + +import java.nio.file.FileSystems; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; + +import static org.junit.Assert.*; + +/** + * Simple tests for glob pattern matching behavior that don't require IntelliJ platform mocking. + * These tests verify the core glob pattern logic that ManifestExclusionManager relies on. + */ +public class ManifestExclusionManagerSimpleTest { + + @Test + public void testGlobPatternBehavior_RootFiles() { + // Test the core issue: ** patterns don't match root files directly + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/pom.xml"); + + // These should behave as expected + assertFalse("**/pom.xml should NOT match root pom.xml directly", + matcher.matches(Paths.get("pom.xml"))); + assertTrue("**/pom.xml should match nested pom.xml", + matcher.matches(Paths.get("src/pom.xml"))); + assertTrue("**/pom.xml should match deeply nested pom.xml", + matcher.matches(Paths.get("src/main/java/pom.xml"))); + } + + @Test + public void testGlobPatternBehavior_WithVirtualPrefix() { + // Test our workaround: adding a virtual directory prefix + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**/pom.xml"); + + // This should work with our virtual prefix approach + assertTrue("**/pom.xml should match dummy/pom.xml (our workaround)", + matcher.matches(Paths.get("dummy/pom.xml"))); + } + + @Test + public void testGlobPatternBehavior_ExactMatch() { + PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:pom.xml"); + + assertTrue("pom.xml pattern should match root pom.xml", + matcher.matches(Paths.get("pom.xml"))); + assertFalse("pom.xml pattern should NOT match nested pom.xml", + matcher.matches(Paths.get("src/pom.xml"))); + } + + @Test + public void testGlobPatternBehavior_DirectoryPatterns() { + PathMatcher testMatcher = FileSystems.getDefault().getPathMatcher("glob:**/test/**"); + + // Test directory exclusion patterns + assertTrue("**/test/** should match files in test directories", + testMatcher.matches(Paths.get("src/test/pom.xml"))); + assertTrue("**/test/** should match nested test directories", + testMatcher.matches(Paths.get("modules/core/test/integration/pom.xml"))); + assertFalse("**/test/** should NOT match files outside test directories", + testMatcher.matches(Paths.get("src/main/pom.xml"))); + } + + @Test + public void testGlobPatternBehavior_NodeModules() { + PathMatcher nodeMatcher = FileSystems.getDefault().getPathMatcher("glob:**/node_modules/**"); + + // Note: **/node_modules/** requires directories before and after node_modules + assertFalse("**/node_modules/** should NOT match root node_modules files (no dir before)", + nodeMatcher.matches(Paths.get("node_modules/lodash/package.json"))); + assertTrue("**/node_modules/** should match nested node_modules", + nodeMatcher.matches(Paths.get("frontend/node_modules/react/package.json"))); + assertFalse("**/node_modules/** should NOT match files outside node_modules", + nodeMatcher.matches(Paths.get("src/package.json"))); + } + + @Test + public void testGlobPatternBehavior_SpecificPaths() { + PathMatcher specificMatcher = FileSystems.getDefault().getPathMatcher("glob:src/test/**/pom.xml"); + + // Note: src/test/**/pom.xml requires at least one directory between test/ and pom.xml + assertFalse("src/test/**/pom.xml should NOT match src/test/pom.xml (no intermediate dir)", + specificMatcher.matches(Paths.get("src/test/pom.xml"))); + assertTrue("src/test/**/pom.xml should match with additional nesting", + specificMatcher.matches(Paths.get("src/test/integration/pom.xml"))); + assertFalse("Specific pattern should NOT match different base paths", + specificMatcher.matches(Paths.get("src/main/pom.xml"))); + assertFalse("Specific pattern should NOT match root files", + specificMatcher.matches(Paths.get("pom.xml"))); + } + + @Test + public void testGlobPatternBehavior_MultipleFileTypes() { + String[] manifestFiles = {"pom.xml", "package.json", "go.mod", "requirements.txt", "build.gradle"}; + + for (String fileName : manifestFiles) { + PathMatcher exactMatcher = FileSystems.getDefault().getPathMatcher("glob:" + fileName); + PathMatcher globalMatcher = FileSystems.getDefault().getPathMatcher("glob:**/" + fileName); + + // Exact patterns should match root files + Path path = Paths.get(fileName); + assertTrue(fileName + " should match at root with exact pattern", + exactMatcher.matches(path)); + + // Global patterns should NOT match root files directly (Java PathMatcher limitation) + assertFalse("**/" + fileName + " should NOT match root " + fileName + " directly", + globalMatcher.matches(path)); + + // But should match with virtual prefix (our workaround) + assertTrue("**/" + fileName + " should match with virtual prefix", + globalMatcher.matches(Paths.get("dummy/" + fileName))); + + // And should match nested files normally + assertTrue("**/" + fileName + " should match nested " + fileName, + globalMatcher.matches(Paths.get("src/" + fileName))); + } + } + + @Test + public void testGlobPatternBehavior_BuildDirectories() { + PathMatcher buildMatcher = FileSystems.getDefault().getPathMatcher("glob:**/build/**"); + PathMatcher targetMatcher = FileSystems.getDefault().getPathMatcher("glob:**/target/**"); + + // Test build directory exclusions (note: **/ requires a directory before build) + assertFalse("**/build/** should NOT match root build files (no dir before)", + buildMatcher.matches(Paths.get("build/libs/pom.xml"))); + assertTrue("**/build/** should match nested build directories", + buildMatcher.matches(Paths.get("modules/core/build/classes/pom.xml"))); + + // Test target directory exclusions (note: **/ requires a directory before target) + assertFalse("**/target/** should NOT match root target files (no dir before)", + targetMatcher.matches(Paths.get("target/classes/pom.xml"))); + assertTrue("**/target/** should match nested target directories", + targetMatcher.matches(Paths.get("modules/core/target/generated/pom.xml"))); + } + + @Test + public void testGlobPatternBehavior_EdgeCases() { + // Test edge cases and special characters + PathMatcher wildcardMatcher = FileSystems.getDefault().getPathMatcher("glob:**"); + + // ** should match everything + assertTrue("** should match root files", + wildcardMatcher.matches(Paths.get("pom.xml"))); + assertTrue("** should match nested files", + wildcardMatcher.matches(Paths.get("src/main/pom.xml"))); + + // Test single directory wildcard + PathMatcher singleMatcher = FileSystems.getDefault().getPathMatcher("glob:*/pom.xml"); + assertFalse("*/pom.xml should NOT match root pom.xml", + singleMatcher.matches(Paths.get("pom.xml"))); + assertTrue("*/pom.xml should match one level deep", + singleMatcher.matches(Paths.get("src/pom.xml"))); + assertFalse("*/pom.xml should NOT match two levels deep", + singleMatcher.matches(Paths.get("src/main/pom.xml"))); + } +} \ No newline at end of file