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-clean-plugin.git

commit d1ecf6e32cb638c498f3e8ffaa8e7921ad990424
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Jul 8 16:11:05 2025 +0200

    Replace `PathSelector` by a modified copy of the class in Maven core.
    The intend is to prepare the replacement by a shared implementation.
---
 .../org/apache/maven/plugins/clean/Cleaner.java    |  11 +-
 .../org/apache/maven/plugins/clean/Fileset.java    |   7 +-
 .../apache/maven/plugins/clean/PathSelector.java   | 508 +++++++++++++++------
 3 files changed, 379 insertions(+), 147 deletions(-)

diff --git a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java 
b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
index 6dd973f..2ad8276 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Cleaner.java
@@ -32,6 +32,7 @@ import java.nio.file.attribute.DosFileAttributeView;
 import java.nio.file.attribute.PosixFileAttributeView;
 import java.nio.file.attribute.PosixFilePermission;
 import java.util.ArrayDeque;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Deque;
 import java.util.EnumSet;
@@ -105,6 +106,10 @@ final class Cleaner implements FileVisitor<Path> {
     @Nonnull
     private final String fastMode;
 
+    /**
+     * Combination of includes and excludes path matchers.
+     * A {@code null} value means to include everything.
+     */
     @Nullable
     private PathSelector selector;
 
@@ -205,7 +210,11 @@ final class Cleaner implements FileVisitor<Path> {
      * @throws IOException if a file/directory could not be deleted and {@code 
failOnError} is {@code true}
      */
     public void delete(@Nonnull Fileset fileset) throws IOException {
-        selector = new PathSelector(fileset);
+        selector = new PathSelector(
+                fileset.getDirectory(),
+                Arrays.asList(fileset.getIncludes()),
+                Arrays.asList(fileset.getExcludes()),
+                fileset.isUseDefaultExcludes());
         if (selector.isEmpty()) {
             selector = null;
         }
diff --git a/src/main/java/org/apache/maven/plugins/clean/Fileset.java 
b/src/main/java/org/apache/maven/plugins/clean/Fileset.java
index 2842ec6..de497f8 100644
--- a/src/main/java/org/apache/maven/plugins/clean/Fileset.java
+++ b/src/main/java/org/apache/maven/plugins/clean/Fileset.java
@@ -41,6 +41,11 @@ public class Fileset {
 
     private boolean useDefaultExcludes;
 
+    /**
+     * Creates an initially empty file set.
+     */
+    public Fileset() {}
+
     /**
      * {@return the base directory}.
      */
@@ -96,7 +101,7 @@ public class Fileset {
      * Appends the elements of the given array in the given buffer.
      * This is a helper method for {@link #toString()} implementations.
      *
-     * @param buffer the buffer where to add the elements
+     * @param buffer the buffer to add the elements to
      * @param label label identifying the array of elements to add
      * @param patterns the elements to append, or {@code null} if none
      */
diff --git a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java 
b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java
index 0a80ba1..2d81082 100644
--- a/src/main/java/org/apache/maven/plugins/clean/PathSelector.java
+++ b/src/main/java/org/apache/maven/plugins/clean/PathSelector.java
@@ -22,19 +22,26 @@ import java.io.File;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
 import java.nio.file.PathMatcher;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
- * Determines whether a path is selected for deletion.
+ * Determines whether a path is selected according to include/exclude patterns.
  * The pathnames used for method parameters will be relative to some base 
directory
- * and use {@code '/'} as separator, regardless the hosting operating system.
+ * and use {@code '/'} as separator, regardless of the hosting operating 
system.
  *
- * <h4>Syntax</h4>
- * If a pattern contains the {@code ':'} character, then that pattern is given 
verbatim to
- * {@link FileSystem#getPathMatcher(String)}, which will interpret the part 
before {@code ':'}
- * as the syntax (usually {@code "glob"} or {@code "regex"}). If a pattern 
does not contain the
- * {@code ':'} character, then the syntax defaults to a reproduction of the 
Maven 3 behavior.
+ * <h2>Syntax</h2>
+ * If a pattern contains the {@code ':'} character and the prefix before is 
longer than 1 character,
+ * then that pattern is given verbatim to {@link 
FileSystem#getPathMatcher(String)}, which interprets
+ * the part before {@code ':'} as the syntax (usually {@code "glob"} or {@code 
"regex"}).
+ * If a pattern does not contain the {@code ':'} character, or if the prefix 
is one character long
+ * (interpreted as a Windows drive), then the syntax defaults to a 
reproduction of the Maven 3 behavior.
  * This is implemented as the {@code "glob"} syntax with the following 
modifications:
  *
  * <ul>
@@ -43,13 +50,17 @@ import java.util.Set;
  *   <li>Trailing {@code "/"} is completed as {@code "/**"}.</li>
  *   <li>The {@code "**"} wildcard means "0 or more directories" instead of "1 
or more directories".
  *       This is implemented by adding variants of the pattern without the 
{@code "**"} wildcard.</li>
+ *   <li>Bracket characters [ ] and { } are escaped.</li>
+ *   <li>On Unix only, the escape character {@code '\\'} is itself 
escaped.</li>
  * </ul>
  *
- * If above changes are not desired, put an explicit {@code "glob:"} prefix 
before the patterns.
- * Note that putting such prefix is recommended anyway for better performances.
+ * If above changes are not desired, put an explicit {@code "glob:"} prefix 
before the pattern.
+ * Note that putting such a prefix is recommended anyway for better 
performances.
  *
  * @author Benjamin Bentmann
  * @author Martin Desruisseaux
+ *
+ * @see java.nio.file.FileSystem#getPathMatcher(String)
  */
 final class PathSelector implements PathMatcher {
     /**
@@ -58,93 +69,126 @@ final class PathSelector implements PathMatcher {
      * <p><b>Source:</b> this list is copied from {@code plexus-utils-4.0.2} 
(released in
      * September 23, 2024), class {@code 
org.codehaus.plexus.util.AbstractScanner}.</p>
      */
-    private static final String[] DEFAULT_EXCLUDES = {
-        // Miscellaneous typical temporary files
-        "**/*~",
-        "**/#*#",
-        "**/.#*",
-        "**/%*%",
-        "**/._*",
-
-        // CVS
-        "**/CVS",
-        "**/CVS/**",
-        "**/.cvsignore",
-
-        // RCS
-        "**/RCS",
-        "**/RCS/**",
-
-        // SCCS
-        "**/SCCS",
-        "**/SCCS/**",
-
-        // Visual SourceSafe
-        "**/vssver.scc",
-
-        // MKS
-        "**/project.pj",
-
-        // Subversion
-        "**/.svn",
-        "**/.svn/**",
-
-        // Arch
-        "**/.arch-ids",
-        "**/.arch-ids/**",
-
-        // Bazaar
-        "**/.bzr",
-        "**/.bzr/**",
-
-        // SurroundSCM
-        "**/.MySCMServerInfo",
-
-        // Mac
-        "**/.DS_Store",
-
-        // Serena Dimensions Version 10
-        "**/.metadata",
-        "**/.metadata/**",
-
-        // Mercurial
-        "**/.hg",
-        "**/.hg/**",
-
-        // git
-        "**/.git",
-        "**/.git/**",
-        "**/.gitignore",
-
-        // BitKeeper
-        "**/BitKeeper",
-        "**/BitKeeper/**",
-        "**/ChangeSet",
-        "**/ChangeSet/**",
-
-        // darcs
-        "**/_darcs",
-        "**/_darcs/**",
-        "**/.darcsrepo",
-        "**/.darcsrepo/**",
-        "**/-darcs-backup*",
-        "**/.darcs-temp-mail"
-    };
+    private static final List<String> DEFAULT_EXCLUDES = List.of(
+            // Miscellaneous typical temporary files
+            "**/*~",
+            "**/#*#",
+            "**/.#*",
+            "**/%*%",
+            "**/._*",
+
+            // CVS
+            "**/CVS",
+            "**/CVS/**",
+            "**/.cvsignore",
+
+            // RCS
+            "**/RCS",
+            "**/RCS/**",
+
+            // SCCS
+            "**/SCCS",
+            "**/SCCS/**",
+
+            // Visual SourceSafe
+            "**/vssver.scc",
+
+            // MKS
+            "**/project.pj",
+
+            // Subversion
+            "**/.svn",
+            "**/.svn/**",
+
+            // Arch
+            "**/.arch-ids",
+            "**/.arch-ids/**",
+
+            // Bazaar
+            "**/.bzr",
+            "**/.bzr/**",
+
+            // SurroundSCM
+            "**/.MySCMServerInfo",
+
+            // Mac
+            "**/.DS_Store",
+
+            // Serena Dimensions Version 10
+            "**/.metadata",
+            "**/.metadata/**",
+
+            // Mercurial
+            "**/.hg",
+            "**/.hg/**",
+
+            // git
+            "**/.git",
+            "**/.git/**",
+            "**/.gitignore",
+
+            // BitKeeper
+            "**/BitKeeper",
+            "**/BitKeeper/**",
+            "**/ChangeSet",
+            "**/ChangeSet/**",
+
+            // darcs
+            "**/_darcs",
+            "**/_darcs/**",
+            "**/.darcsrepo",
+            "**/.darcsrepo/**",
+            "**/-darcs-backup*",
+            "**/.darcs-temp-mail");
+
+    /**
+     * Maximum number of characters of the prefix before {@code ':'} for 
handling as a Maven syntax.
+     */
+    private static final int MAVEN_SYNTAX_THRESHOLD = 1;
 
     /**
-     * String representation of the normalized include filters.
-     * This is kept only for {@link #toString()} implementation.
+     * The default syntax to use if none was specified. Note that when this 
default syntax is applied,
+     * the user-provided pattern get some changes as documented in class 
Javadoc.
+     */
+    private static final String DEFAULT_SYNTAX = "glob:";
+
+    /**
+     * Characters having a special meaning in the glob syntax.
+     *
+     * @see FileSystem#getPathMatcher(String)
+     */
+    private static final String SPECIAL_CHARACTERS = "*?[]{}\\";
+
+    /**
+     * A path matcher which accepts all files.
+     *
+     * @see #simplify()
+     */
+    private static final PathMatcher INCLUDES_ALL = (path) -> true;
+
+    /**
+     * String representations of the normalized include filters.
+     * Each pattern shall be prefixed by its syntax, which is {@value 
#DEFAULT_SYNTAX} by default.
+     * An empty array means to include all files.
+     *
+     * @see #toString()
      */
     private final String[] includePatterns;
 
     /**
-     * String representation of the normalized exclude filters.
-     * This is kept only for {@link #toString()} implementation.
+     * String representations of the normalized exclude filters.
+     * Each pattern shall be prefixed by its syntax. If no syntax is specified,
+     * the default is a Maven 3 syntax similar, but not identical, to {@value 
#DEFAULT_SYNTAX}.
+     * This array may be longer or shorter than the user-supplied excludes, 
depending on whether
+     * default excludes have been added and whether some unnecessary excludes 
have been omitted.
+     *
+     * @see #toString()
      */
     private final String[] excludePatterns;
 
     /**
      * The matcher for includes. The length of this array is equal to {@link 
#includePatterns} array length.
+     * An empty array means to include all files.
      */
     private final PathMatcher[] includes;
 
@@ -157,12 +201,13 @@ final class PathSelector implements PathMatcher {
      * The matcher for all directories to include. This array includes the 
parents of all those directories,
      * because they need to be accepted before we can walk to the 
sub-directories.
      * This is an optimization for skipping whole directories when possible.
+     * An empty array means to include all directories.
      */
     private final PathMatcher[] dirIncludes;
 
     /**
      * The matcher for directories to exclude. This array does <em>not</em> 
include the parent directories,
-     * since they may contain other sub-trees that need to be included.
+     * because they may contain other sub-trees that need to be included.
      * This is an optimization for skipping whole directories when possible.
      */
     private final PathMatcher[] dirExcludes;
@@ -173,41 +218,184 @@ final class PathSelector implements PathMatcher {
     private final Path baseDirectory;
 
     /**
-     * Creates a new selector from the given file seT.
+     * Whether paths must be relativized before to be given to a matcher. If 
{@code true}, then every paths
+     * will be made relative to {@link #baseDirectory} for allowing patterns 
like {@code "foo/bar/*.java"}
+     * to work. As a slight optimization, we can skip this step if all 
patterns start with {@code "**"}.
+     */
+    private final boolean needRelativize;
+
+    /**
+     * Creates a new selector from the given includes and excludes.
      *
-     * @param fs the user-specified configuration
+     * @param directory the base directory of the files to filter
+     * @param includes the patterns of the files to include, or null or empty 
for including all files
+     * @param excludes the patterns of the files to exclude, or null or empty 
for no exclusion
+     * @param useDefaultExcludes whether to augment the excludes with a 
default set of <abbr>SCM</abbr> patterns
      */
-    PathSelector(Fileset fs) {
-        includePatterns = normalizePatterns(fs.getIncludes(), false);
-        excludePatterns = 
normalizePatterns(addDefaultExcludes(fs.getExcludes(), 
fs.isUseDefaultExcludes()), true);
-        baseDirectory = fs.getDirectory();
-        FileSystem system = baseDirectory.getFileSystem();
-        includes = matchers(system, includePatterns);
-        excludes = matchers(system, excludePatterns);
-        dirIncludes = matchers(system, directoryPatterns(includePatterns, 
false));
-        dirExcludes = matchers(system, directoryPatterns(excludePatterns, 
true));
+    PathSelector(Path directory, Collection<String> includes, 
Collection<String> excludes, boolean useDefaultExcludes) {
+        includePatterns = normalizePatterns(includes, false);
+        excludePatterns = normalizePatterns(effectiveExcludes(excludes, 
includePatterns, useDefaultExcludes), true);
+        baseDirectory = directory;
+        FileSystem fs = directory.getFileSystem();
+        this.includes = matchers(fs, includePatterns);
+        this.excludes = matchers(fs, excludePatterns);
+        dirIncludes = matchers(fs, directoryPatterns(includePatterns, false));
+        dirExcludes = matchers(fs, directoryPatterns(excludePatterns, true));
+        needRelativize = needRelativize(includePatterns) || 
needRelativize(excludePatterns);
     }
 
     /**
-     * Returns the given array of excludes, optionally expanded with a default 
set of excludes.
+     * Returns the given array of excludes, optionally expanded with a default 
set of excludes,
+     * then with unnecessary excludes omitted. An unnecessary exclude is an 
exclude which will never
+     * match a file because there is no include which would accept a file that 
could match the exclude.
+     * For example, if the only include is {@code "*.java"}, then the 
<code>"**&sol;project.pj"</code>,
+     * <code>"**&sol;.DS_Store"</code> and other excludes will never match a 
file and can be omitted.
+     * Because the list of {@linkplain #DEFAULT_EXCLUDES default excludes} 
contains many elements,
+     * removing unnecessary excludes can reduce a lot the number of matches 
tested on each source file.
+     *
+     * <h4>Implementation note</h4>
+     * The removal of unnecessary excludes is done on a best effort basis. The 
current implementation
+     * compares only the prefixes and suffixes of each pattern, keeping the 
pattern in case of doubt.
+     * This is not bad, but it does not remove all unnecessary patterns. It 
would be possible to do
+     * better in the future if benchmarking suggests that it would be worth 
the effort.
      *
-     * @param excludes the user-specified excludes.
+     * @param excludes the user-specified excludes, potentially not yet 
converted to glob syntax
+     * @param includes the include patterns converted to glob syntax
      * @param useDefaultExcludes whether to expand user exclude with the set 
of default excludes
-     * @return the potentially expanded set of excludes to use
+     * @return the potentially expanded or reduced set of excludes to use
      */
-    private static String[] addDefaultExcludes(final String[] excludes, final 
boolean useDefaultExcludes) {
-        if (!useDefaultExcludes) {
+    private static Collection<String> effectiveExcludes(
+            Collection<String> excludes, final String[] includes, final 
boolean useDefaultExcludes) {
+        if (excludes == null || excludes.isEmpty()) {
+            if (useDefaultExcludes) {
+                excludes = new ArrayList<>(DEFAULT_EXCLUDES);
+            } else {
+                return List.of();
+            }
+        } else {
+            excludes = new ArrayList<>(excludes);
+            excludes.removeIf(Objects::isNull);
+            if (useDefaultExcludes) {
+                excludes.addAll(DEFAULT_EXCLUDES);
+            }
+        }
+        if (includes.length == 0) {
             return excludes;
         }
-        String[] defaults = DEFAULT_EXCLUDES;
-        if (excludes == null || excludes.length == 0) {
-            return defaults;
-        } else {
-            String[] patterns = new String[excludes.length + defaults.length];
-            System.arraycopy(excludes, 0, patterns, 0, excludes.length);
-            System.arraycopy(defaults, 0, patterns, excludes.length, 
defaults.length);
-            return patterns;
+        /*
+         * Get the prefixes and suffixes of all includes, stopping at the 
first special character.
+         * Redundant prefixes and suffixes are omitted.
+         */
+        var prefixes = new String[includes.length];
+        var suffixes = new String[includes.length];
+        for (int i = 0; i < includes.length; i++) {
+            String include = includes[i];
+            if (!include.startsWith(DEFAULT_SYNTAX)) {
+                return excludes; // Do not filter if at least one pattern is 
too complicated.
+            }
+            include = include.substring(DEFAULT_SYNTAX.length());
+            prefixes[i] = prefixOrSuffix(include, false);
+            suffixes[i] = prefixOrSuffix(include, true);
+        }
+        prefixes = sortByLength(prefixes, false);
+        suffixes = sortByLength(suffixes, true);
+        /*
+         * Keep only the exclude which start with one of the prefixes and end 
with one of the suffixes.
+         * Note that a prefix or suffix may be the empty string, which match 
everything.
+         */
+        final Iterator<String> it = excludes.iterator();
+        nextExclude:
+        while (it.hasNext()) {
+            final String exclude = it.next();
+            final int s = exclude.indexOf(':');
+            if (s <= MAVEN_SYNTAX_THRESHOLD || 
exclude.startsWith(DEFAULT_SYNTAX)) {
+                if (cannotMatch(exclude, prefixes, false) || 
cannotMatch(exclude, suffixes, true)) {
+                    it.remove();
+                }
+            }
+        }
+        return excludes;
+    }
+
+    /**
+     * Returns the maximal amount of ordinary characters at the beginning or 
end of the given pattern.
+     * The prefix or suffix stops at the first {@linkplain #SPECIAL_CHARACTERS 
special character}.
+     *
+     * @param include the pattern for which to get a prefix or suffix without 
special character
+     * @param suffix {@code false} if a prefix is desired, or {@code true} if 
a suffix is desired
+     */
+    private static String prefixOrSuffix(final String include, boolean suffix) 
{
+        int s = suffix ? -1 : include.length();
+        for (int i = SPECIAL_CHARACTERS.length(); --i >= 0; ) {
+            char c = SPECIAL_CHARACTERS.charAt(i);
+            if (suffix) {
+                s = Math.max(s, include.lastIndexOf(c));
+            } else {
+                int p = include.indexOf(c);
+                if (p >= 0 && p < s) {
+                    s = p;
+                }
+            }
         }
+        return suffix ? include.substring(s + 1) : include.substring(0, s);
+    }
+
+    /**
+     * Returns {@code true} if the given exclude cannot match any include 
patterns.
+     * In case of doubt, returns {@code false}.
+     *
+     * @param exclude the exclude pattern to test
+     * @param fragments the prefixes or suffixes (fragments without special 
characters) of the includes
+     * @param suffix {@code false} if the specified fragments are prefixes, 
{@code true} if they are suffixes
+     * @return {@code true} if it is certain that the exclude pattern cannot 
match, or {@code false} in case of doubt
+     */
+    private static boolean cannotMatch(String exclude, final String[] 
fragments, final boolean suffix) {
+        exclude = prefixOrSuffix(exclude, suffix);
+        for (String fragment : fragments) {
+            int fg = fragment.length();
+            int ex = exclude.length();
+            int length = Math.min(fg, ex);
+            if (suffix) {
+                fg -= length;
+                ex -= length;
+            } else {
+                fg = 0;
+                ex = 0;
+            }
+            if (exclude.regionMatches(ex, fragment, fg, length)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Sorts the given patterns by their length. The main intent is to have 
the empty string first,
+     * while will cause the loops testing for prefixes and suffixes to stop 
almost immediately.
+     * Short prefixes or suffixes are also more likely to be matched.
+     *
+     * @param fragments the fragments to sort in-place
+     * @param suffix {@code false} if the specified fragments are prefixes, 
{@code true} if they are suffixes
+     * @return the given array, or a smaller array if some fragments were 
discarded because redundant
+     */
+    private static String[] sortByLength(final String[] fragments, final 
boolean suffix) {
+        Arrays.sort(fragments, (s1, s2) -> s1.length() - s2.length());
+        int count = 0;
+        /*
+         * Simplify the array of prefixes or suffixes by removing all 
redundant elements.
+         * An element is redundant if there is a shorter prefix or suffix with 
the same characters.
+         */
+        nextBase:
+        for (String fragment : fragments) {
+            for (int i = count; --i >= 0; ) {
+                String base = fragments[i];
+                if (suffix ? fragment.endsWith(base) : 
fragment.startsWith(base)) {
+                    continue nextBase; // Skip this fragment
+                }
+            }
+            fragments[count++] = fragment;
+        }
+        return (fragments.length == count) ? fragments : 
Arrays.copyOf(fragments, count);
     }
 
     /**
@@ -218,16 +406,15 @@ final class PathSelector implements PathMatcher {
      * @param excludes whether the patterns are exclude patterns
      * @return normalized patterns without null, empty or duplicated patterns
      */
-    private static String[] normalizePatterns(final String[] patterns, final 
boolean excludes) {
-        if (patterns == null) {
+    private static String[] normalizePatterns(final Collection<String> 
patterns, final boolean excludes) {
+        if (patterns == null || patterns.isEmpty()) {
             return new String[0];
         }
         // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
-        final var normalized = new LinkedHashSet<String>(patterns.length);
+        final var normalized = new LinkedHashSet<String>(patterns.size());
         for (String pattern : patterns) {
             if (pattern != null && !pattern.isEmpty()) {
-                final boolean useMavenSyntax = pattern.indexOf(':') < 0;
-                if (useMavenSyntax) {
+                if (pattern.indexOf(':') <= MAVEN_SYNTAX_THRESHOLD) {
                     pattern = pattern.replace(File.separatorChar, '/');
                     if (pattern.endsWith("/")) {
                         pattern += "**";
@@ -240,15 +427,20 @@ final class PathSelector implements PathMatcher {
                         pattern = pattern.substring(3);
                     }
                     pattern = pattern.replace("/**/**/", "/**/");
-                }
-                normalized.add(pattern);
-                /*
-                 * If the pattern starts or ends with "**", Java GLOB expects 
a directory level at
-                 * that location while Maven seems to consider that "**" can 
mean "no directory".
-                 * Add another pattern for reproducing this effect.
-                 */
-                if (useMavenSyntax) {
+                    pattern = pattern.replace("\\", "\\\\")
+                            .replace("[", "\\[")
+                            .replace("]", "\\]")
+                            .replace("{", "\\{")
+                            .replace("}", "\\}");
+                    normalized.add(DEFAULT_SYNTAX + pattern);
+                    /*
+                     * If the pattern starts or ends with "**", Java GLOB 
expects a directory level at
+                     * that location while Maven seems to consider that "**" 
can mean "no directory".
+                     * Add another pattern for reproducing this effect.
+                     */
                     addPatternsWithOneDirRemoved(normalized, pattern, 0);
+                } else {
+                    normalized.add(pattern);
                 }
             }
         }
@@ -261,7 +453,7 @@ final class PathSelector implements PathMatcher {
      * Tests suggest that we need an explicit GLOB pattern with no {@code 
"**"} for matching an absence of directory.
      *
      * @param patterns where to add the derived patterns
-     * @param pattern  the pattern for which to add derived forms
+     * @param pattern  the pattern for which to add derived forms, without the 
"glob:" syntax prefix
      * @param end      should be 0 (reserved for recursive invocations of this 
method)
      */
     private static void addPatternsWithOneDirRemoved(final Set<String> 
patterns, final String pattern, int end) {
@@ -277,13 +469,11 @@ final class PathSelector implements PathMatcher {
                     end++; // Ommit the leading slash if there is nothing 
before it.
                 }
             }
-            if (start > 0) {
-                if (pattern.charAt(--start) != '/') {
-                    continue;
-                }
+            if (start > 0 && pattern.charAt(--start) != '/') {
+                continue;
             }
             String reduced = pattern.substring(0, start) + 
pattern.substring(end);
-            patterns.add(reduced);
+            patterns.add(DEFAULT_SYNTAX + reduced);
             addPatternsWithOneDirRemoved(patterns, reduced, start);
         }
     }
@@ -292,7 +482,7 @@ final class PathSelector implements PathMatcher {
      * Applies some heuristic rules for simplifying the set of patterns,
      * then returns the patterns as an array.
      *
-     * @param patterns the patterns to simplify and return asarray
+     * @param patterns the patterns to simplify and return as an array
      * @param excludes whether the patterns are exclude patterns
      * @return the set content as an array, after simplification
      */
@@ -322,13 +512,13 @@ final class PathSelector implements PathMatcher {
         // TODO: use `LinkedHashSet.newLinkedHashSet(int)` instead with JDK19.
         final var directories = new LinkedHashSet<String>(patterns.length);
         for (String pattern : patterns) {
-            int s = pattern.indexOf(':');
-            if (s < 0 || pattern.startsWith("glob:")) {
+            if (pattern.startsWith(DEFAULT_SYNTAX)) {
                 if (excludes) {
                     if (pattern.endsWith("/**")) {
                         directories.add(pattern.substring(0, pattern.length() 
- 3));
                     }
                 } else {
+                    int s = pattern.indexOf(':');
                     if (pattern.regionMatches(++s, "**/", 0, 3)) {
                         s = pattern.indexOf('/', s + 3);
                         if (s < 0) {
@@ -342,18 +532,29 @@ final class PathSelector implements PathMatcher {
         return simplify(directories, excludes);
     }
 
+    /**
+     * Returns {@code true} if at least one pattern requires path to be 
relativized before to be matched.
+     *
+     * @param patterns include or exclude patterns
+     * @return whether at least one pattern require relativization
+     */
+    private static boolean needRelativize(String[] patterns) {
+        for (String pattern : patterns) {
+            if (!pattern.startsWith(DEFAULT_SYNTAX + "**/")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Creates the path matchers for the given patterns.
-     * If no syntax is specified, the default is {@code glob}.
+     * The syntax (usually {@value #DEFAULT_SYNTAX}) must be specified for 
each pattern.
      */
     private static PathMatcher[] matchers(final FileSystem fs, final String[] 
patterns) {
         final var matchers = new PathMatcher[patterns.length];
         for (int i = 0; i < patterns.length; i++) {
-            String pattern = patterns[i];
-            if (pattern.indexOf(':') < 0) {
-                pattern = "glob:" + pattern;
-            }
-            matchers[i] = fs.getPathMatcher(pattern);
+            matchers[i] = fs.getPathMatcher(patterns[i]);
         }
         return matchers;
     }
@@ -362,26 +563,44 @@ final class PathSelector implements PathMatcher {
      * {@return whether there is no include or exclude filters}.
      * In such case, this {@code PathSelector} instance should be ignored.
      */
-    boolean isEmpty() {
+    public boolean isEmpty() {
         return (includes.length | excludes.length) == 0;
     }
 
     /**
-     * Determines whether a path is selected for deletion.
+     * {@return a potentially simpler matcher equivalent to this matcher}.
+     */
+    @SuppressWarnings("checkstyle:MissingSwitchDefault")
+    public PathMatcher simplify() {
+        if (!needRelativize && excludes.length == 0) {
+            switch (includes.length) {
+                case 0:
+                    return INCLUDES_ALL;
+                case 1:
+                    return includes[0];
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Determines whether a path is selected.
      * This is true if the given file matches an include pattern and no 
exclude pattern.
      *
-     * @param pathname The pathname to test, must not be {@code null}
-     * @return {@code true} if the given path is selected for deletion, {@code 
false} otherwise
+     * @param path the pathname to test, must not be {@code null}
+     * @return {@code true} if the given path is selected, {@code false} 
otherwise
      */
     @Override
     public boolean matches(Path path) {
-        path = baseDirectory.relativize(path);
+        if (needRelativize) {
+            path = baseDirectory.relativize(path);
+        }
         return (includes.length == 0 || isMatched(path, includes))
                 && (excludes.length == 0 || !isMatched(path, excludes));
     }
 
     /**
-     * {@return whether the given file matches according one of the given 
matchers}.
+     * {@return whether the given file matches according to one of the given 
matchers}.
      */
     private static boolean isMatched(Path path, PathMatcher[] matchers) {
         for (PathMatcher matcher : matchers) {
@@ -410,7 +629,6 @@ final class PathSelector implements PathMatcher {
 
     /**
      * {@return a string representation for logging purposes}.
-     * This string representation is reported logged when {@link Cleaner} is 
executed.
      */
     @Override
     public String toString() {

Reply via email to