You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/08/09 13:37:23 UTC
(maven-plugin-tools) 01/02: Fix excludedScanDirectories never matching on Windows
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-plugin-tools.git
commit b791f400d1c424f17968beb0148155ab66390acc
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Sun Aug 9 12:18:45 2026 +0200
Fix excludedScanDirectories never matching on Windows
isExcludedScanDirectory() built a glob straight out of the configured value.
A backslash is the escape character of the glob syntax and sun.nio.fs.Globs
applies that on Windows too, so a value interpolated from ${project.basedir}
or ${project.build.directory} lost every separator: the pattern degenerated to
"D:aprojtarget" and could never match a real path, and a value ending in a
separator failed the build outright with a PatternSyntaxException. The feature
therefore did nothing on Windows for the documented usage.
Compare as paths first, which is separator-, case- and trailing-separator-safe,
and translate backslashes to forward slashes before building the glob on file
systems that separate names with a backslash. A forward slash is compiled back
into the Windows separator, so globs keep working as documented on both
platforms; on file systems where a backslash is a legal name character it is
left alone, since there it may be a deliberate escape.
The matching logic moves into a package-private method taking a Path so the
Windows behaviour can be exercised without running on Windows.
Generated-by: Claude Code (Claude Opus 5, 1M context)
---
.../tools/plugin/DefaultPluginToolsRequest.java | 63 ++++++++-
.../plugin/DefaultPluginToolsRequestTest.java | 148 +++++++++++++++++++++
2 files changed, 207 insertions(+), 4 deletions(-)
diff --git a/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequest.java b/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequest.java
index e1009aa9..317c354b 100644
--- a/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequest.java
+++ b/maven-plugin-tools-api/src/main/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequest.java
@@ -21,6 +21,7 @@ package org.apache.maven.tools.plugin;
import java.io.File;
import java.net.URI;
import java.nio.file.FileSystem;
+import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashSet;
@@ -254,13 +255,67 @@ public class DefaultPluginToolsRequest implements PluginToolsRequest {
@Override
public boolean isExcludedScanDirectory(File sourceFile) {
- Path sourcePath = sourceFile.toPath();
- FileSystem sourceFs = sourcePath.getFileSystem();
- for (String excludedScanDirectory : getExcludedScanDirectories()) {
- if (sourceFs.getPathMatcher("glob:" + excludedScanDirectory).matches(sourcePath)) {
+ return isExcluded(sourceFile.toPath(), getExcludedScanDirectories());
+ }
+
+ /**
+ * Determines whether a source directory is covered by any of the configured exclusions.
+ * <p>
+ * Visible for testing: taking the directory as a {@link Path} keeps the whole decision tied to a single
+ * {@link FileSystem}, so the behaviour on Windows separators can be exercised without running on Windows.
+ *
+ * @param sourceDirectory the source directory to check
+ * @param excludedScanDirectories the configured exclusions, either plain directories or globs
+ * @return true if excluded, false otherwise
+ */
+ static boolean isExcluded(Path sourceDirectory, Collection<String> excludedScanDirectories) {
+ FileSystem fileSystem = sourceDirectory.getFileSystem();
+ boolean windowsSeparators = "\\".equals(fileSystem.getSeparator());
+ Path sourcePath = sourceDirectory.toAbsolutePath().normalize();
+ for (String excludedScanDirectory : excludedScanDirectories) {
+ if (excludedScanDirectory == null || excludedScanDirectory.isEmpty()) {
+ // an empty entry would resolve to the working directory and exclude it silently
+ continue;
+ }
+ // The documented form of this parameter is a plain directory, so compare as paths first. That
+ // is safe with respect to separators, trailing separators and case on every file system.
+ try {
+ Path excludedPath = fileSystem
+ .getPath(excludedScanDirectory)
+ .toAbsolutePath()
+ .normalize();
+ if (sourcePath.equals(excludedPath)) {
+ return true;
+ }
+ } catch (InvalidPathException e) {
+ // not a plain directory - the Windows parser rejects wildcards, for instance - so it can
+ // only ever have been meant as a glob
+ }
+ if (fileSystem
+ .getPathMatcher("glob:" + toGlobPattern(excludedScanDirectory, windowsSeparators))
+ .matches(sourcePath)) {
return true;
}
}
return false;
}
+
+ /**
+ * Turns a configured exclusion into a glob pattern.
+ * <p>
+ * Configured values normally come from an interpolated property such as
+ * <code>${project.build.directory}/generated-sources/annotations</code> and therefore hold backslashes on
+ * Windows. A backslash is the escape character of the glob syntax and is swallowed by the pattern compiler
+ * on every platform, so such a value could never match a real path, and one ending in a separator even
+ * failed the build with a {@link java.util.regex.PatternSyntaxException}. A forward slash is compiled back
+ * into the Windows separator, so it is the portable way to spell a separator in a glob.
+ *
+ * @param excludedScanDirectory the configured exclusion
+ * @param windowsSeparators whether the file system separates names with a backslash
+ * @return the pattern to hand to {@link FileSystem#getPathMatcher(String)}
+ */
+ static String toGlobPattern(String excludedScanDirectory, boolean windowsSeparators) {
+ // On a file system that allows backslashes in names they may be a deliberate escape, so leave them be.
+ return windowsSeparators ? excludedScanDirectory.replace('\\', '/') : excludedScanDirectory;
+ }
}
diff --git a/maven-plugin-tools-api/src/test/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequestTest.java b/maven-plugin-tools-api/src/test/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequestTest.java
new file mode 100644
index 00000000..7f0b80be
--- /dev/null
+++ b/maven-plugin-tools-api/src/test/java/org/apache/maven/tools/plugin/DefaultPluginToolsRequestTest.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.tools.plugin;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DefaultPluginToolsRequestTest {
+
+ private static final Path GENERATED_SOURCES =
+ Paths.get("project", "target", "generated-sources", "annotations").toAbsolutePath();
+
+ private PluginToolsRequest newRequest() {
+ return new DefaultPluginToolsRequest(new MavenProject(), new PluginDescriptor());
+ }
+
+ @Test
+ void aConfiguredSourceRootIsExcluded() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(Collections.singleton(GENERATED_SOURCES.toString()));
+
+ assertTrue(request.isExcludedScanDirectory(GENERATED_SOURCES.toFile()));
+ }
+
+ @Test
+ void aTrailingSeparatorStillExcludes() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(Collections.singleton(
+ GENERATED_SOURCES + GENERATED_SOURCES.getFileSystem().getSeparator()));
+
+ assertTrue(request.isExcludedScanDirectory(GENERATED_SOURCES.toFile()));
+ }
+
+ @Test
+ void otherSourceRootsAreNotExcluded() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(Collections.singleton(GENERATED_SOURCES.toString()));
+
+ assertFalse(request.isExcludedScanDirectory(
+ Paths.get("project", "src", "main", "java").toAbsolutePath().toFile()));
+ }
+
+ @Test
+ void aSiblingOfAnExcludedDirectoryIsNotExcluded() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(Collections.singleton(GENERATED_SOURCES.toString()));
+
+ assertFalse(request.isExcludedScanDirectory(
+ Paths.get(GENERATED_SOURCES + "2").toFile()));
+ }
+
+ @Test
+ void anyOfTheConfiguredDirectoriesExcludes() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(Arrays.asList(
+ Paths.get("project", "target", "generated-test-sources", "annotations")
+ .toAbsolutePath()
+ .toString(),
+ GENERATED_SOURCES.toString()));
+
+ assertTrue(request.isExcludedScanDirectory(GENERATED_SOURCES.toFile()));
+ }
+
+ @Test
+ void nothingIsExcludedByDefault() {
+ assertFalse(newRequest().isExcludedScanDirectory(GENERATED_SOURCES.toFile()));
+ }
+
+ @Test
+ void anEmptyEntryExcludesNothing() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(Collections.singleton(""));
+
+ // an empty entry resolves to the working directory, which must not exclude anything
+ assertFalse(
+ request.isExcludedScanDirectory(Paths.get("").toAbsolutePath().toFile()));
+ }
+
+ /**
+ * The regression this class was written for: a configured directory built from an interpolated property
+ * such as {@code ${project.build.directory}} holds Windows separators on Windows, and a backslash is the
+ * escape character of the glob syntax. Left as-is it is swallowed, so the pattern could never match and
+ * the exclusion silently did nothing.
+ */
+ @Test
+ void windowsSeparatorsBecomeGlobSeparators() {
+ assertEquals(
+ "C:/project/target/generated-sources/annotations",
+ DefaultPluginToolsRequest.toGlobPattern("C:\\project\\target\\generated-sources\\annotations", true));
+ }
+
+ @Test
+ void windowsSeparatorsAreTranslatedWithoutDisturbingWildcards() {
+ assertEquals(
+ "C:/project/target/generated-sources/*",
+ DefaultPluginToolsRequest.toGlobPattern("C:\\project\\target\\generated-sources\\*", true));
+ }
+
+ @Test
+ void aTrailingWindowsSeparatorNoLongerBreaksThePattern() {
+ // a glob may not end in an escape character, which used to fail the build with a
+ // PatternSyntaxException rather than merely failing to match
+ assertEquals("C:/project/target/", DefaultPluginToolsRequest.toGlobPattern("C:\\project\\target\\", true));
+ }
+
+ @Test
+ void backslashesAreLeftAloneWhereTheyAreNotSeparators() {
+ // on such a file system a backslash is a legal name character and a deliberate glob escape
+ assertEquals(
+ "/project/target/generated-sources/\\*",
+ DefaultPluginToolsRequest.toGlobPattern("/project/target/generated-sources/\\*", false));
+ }
+
+ @Test
+ void globsStillMatch() {
+ PluginToolsRequest request = newRequest();
+ request.setExcludedScanDirectories(
+ Collections.singleton(GENERATED_SOURCES.getParent().resolve("*").toString()));
+
+ assertTrue(request.isExcludedScanDirectory(GENERATED_SOURCES.toFile()));
+ }
+}