This is an automated email from the ASF dual-hosted git repository. gnodet pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/master by this push: new f01db87731 Simplify "**" handling using brace expansion (#11125) f01db87731 is described below commit f01db87731a47619fee053b0597668f6ad297660 Author: Guillaume Nodet <gno...@gmail.com> AuthorDate: Wed Sep 17 07:59:04 2025 +0200 Simplify "**" handling using brace expansion (#11125) - Convert "**/" to "{**/,}" after escaping special chars (including braces) - Treat user braces literally in Maven-style patterns; alternation remains with explicit glob: - Add tests for literal braces and explicit glob alternation Fixes #11110 --- .../java/org/apache/maven/impl/PathSelector.java | 46 +++----------- .../org/apache/maven/impl/PathSelectorTest.java | 71 +++++++++++++++++++++- 2 files changed, 79 insertions(+), 38 deletions(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/PathSelector.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/PathSelector.java index 20d68cd7bb..a8cc3da2ec 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/PathSelector.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/PathSelector.java @@ -449,18 +449,21 @@ private static String[] normalizePatterns(final Collection<String> patterns, fin pattern = pattern.substring(3); } pattern = pattern.replace("/**/**/", "/**/"); + + // Escape special characters, including braces + // Braces from user input must be literals; we'll inject our own braces for expansion below pattern = pattern.replace("\\", "\\\\") .replace("[", "\\[") .replace("]", "\\]") .replace("{", "\\{") .replace("}", "\\}"); + + // Transform ** patterns to use brace expansion for POSIX behavior + // This replaces the complex addPatternsWithOneDirRemoved logic + // We perform this after escaping so that only these injected braces participate in expansion + pattern = pattern.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); } @@ -469,37 +472,6 @@ private static String[] normalizePatterns(final Collection<String> patterns, fin return simplify(normalized, excludes); } - /** - * Adds all variants of the given pattern with {@code **} removed. - * This is used for simulating the Maven behavior where {@code "**} may match zero directory. - * 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, 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) { - final int length = pattern.length(); - int start; - while ((start = pattern.indexOf("**", end)) >= 0) { - end = start + 2; // 2 is the length of "**". - if (end < length) { - if (pattern.charAt(end) != '/') { - continue; - } - if (start == 0) { - end++; // Ommit the leading slash if there is nothing before it. - } - } - if (start > 0 && pattern.charAt(--start) != '/') { - continue; - } - String reduced = pattern.substring(0, start) + pattern.substring(end); - patterns.add(DEFAULT_SYNTAX + reduced); - addPatternsWithOneDirRemoved(patterns, reduced, start); - } - } - /** * Applies some heuristic rules for simplifying the set of patterns, * then returns the patterns as an array. diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/PathSelectorTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/PathSelectorTest.java index c7d860bc0b..f541ee5e40 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/PathSelectorTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/PathSelectorTest.java @@ -86,8 +86,77 @@ public void testExcludeOmission() { List<String> excludes = List.of("baz/**"); PathMatcher matcher = PathSelector.of(directory, includes, excludes, true); String s = matcher.toString(); - assertTrue(s.contains("glob:**/*.java")); + assertTrue(s.contains("glob:**/*.java") || s.contains("glob:{**/,}*.java")); assertFalse(s.contains("project.pj")); // Unnecessary exclusion should have been omitted. assertFalse(s.contains(".DS_Store")); } + + /** + * Test to verify the current behavior of ** patterns before implementing brace expansion improvement. + * This test documents the expected behavior that must be preserved after the optimization. + */ + @Test + public void testDoubleAsteriskPatterns(final @TempDir Path directory) throws IOException { + // Create a nested directory structure to test ** behavior + Path src = Files.createDirectory(directory.resolve("src")); + Path main = Files.createDirectory(src.resolve("main")); + Path java = Files.createDirectory(main.resolve("java")); + Path test = Files.createDirectory(src.resolve("test")); + Path testJava = Files.createDirectory(test.resolve("java")); + + // Create files at different levels + Files.createFile(directory.resolve("root.java")); + Files.createFile(src.resolve("src.java")); + Files.createFile(main.resolve("main.java")); + Files.createFile(java.resolve("deep.java")); + Files.createFile(test.resolve("test.java")); + Files.createFile(testJava.resolve("testdeep.java")); + + // Test that ** matches zero or more directories (POSIX behavior) + PathMatcher matcher = PathSelector.of(directory, List.of("src/**/test/**/*.java"), null, false); + + // Should match files in src/test/java/ (** matches zero dirs before test, zero dirs after test) + assertTrue(matcher.matches(testJava.resolve("testdeep.java"))); + + // Should also match files directly in src/test/ (** matches zero dirs after test) + assertTrue(matcher.matches(test.resolve("test.java"))); + + // Should NOT match files in other paths + assertFalse(matcher.matches(directory.resolve("root.java"))); + assertFalse(matcher.matches(src.resolve("src.java"))); + assertFalse(matcher.matches(main.resolve("main.java"))); + assertFalse(matcher.matches(java.resolve("deep.java"))); + } + + @Test + public void testLiteralBracesAreEscapedInMavenSyntax(@TempDir Path directory) throws IOException { + // Create a file with literal braces in the name + Files.createDirectories(directory.resolve("dir")); + Path file = directory.resolve("dir/foo{bar}.txt"); + Files.createFile(file); + + // In Maven syntax (no explicit glob:), user-provided braces must be treated literally + PathMatcher matcher = PathSelector.of(directory, List.of("**/foo{bar}.txt"), null, false); + + assertTrue(matcher.matches(file)); + } + + @Test + public void testBraceAlternationOnlyWithExplicitGlob(@TempDir Path directory) throws IOException { + // Create src/main/java and src/test/java with files + Path mainJava = Files.createDirectories(directory.resolve("src/main/java")); + Path testJava = Files.createDirectories(directory.resolve("src/test/java")); + Path mainFile = Files.createFile(mainJava.resolve("Main.java")); + Path testFile = Files.createFile(testJava.resolve("Test.java")); + + // Without explicit glob:, braces from user input are escaped and treated literally -> no matches + PathMatcher mavenSyntax = PathSelector.of(directory, List.of("src/{main,test}/**/*.java"), null, false); + assertFalse(mavenSyntax.matches(mainFile)); + assertFalse(mavenSyntax.matches(testFile)); + + // With explicit glob:, braces should act as alternation and match both + PathMatcher explicitGlob = PathSelector.of(directory, List.of("glob:src/{main,test}/**/*.java"), null, false); + assertTrue(explicitGlob.matches(mainFile)); + assertTrue(explicitGlob.matches(testFile)); + } }