Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: replace PathMatcher with handmade matcher #61

Merged
merged 7 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 268 additions & 33 deletions hawkeye-core/src/main/java/io/korandoru/hawkeye/core/Selection.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
Expand Down Expand Up @@ -75,41 +75,51 @@ public String[] getSelectedFiles() {

final Path basePath = basedir.toPath();

final List<String> excludesList = new ArrayList<>();
final List<String> invertExcludesList = new ArrayList<>();
final List<String[]> excludeList = new ArrayList<>();
final List<String[]> includeList = new ArrayList<>();
final List<String[]> invertExcludeList = new ArrayList<>();

for (String exclude : excluded) {
String pattern = exclude;
if (pattern.startsWith("!")) {
pattern = pattern.substring(1);
}
if (pattern.endsWith("/")) {
pattern = pattern.substring(0, pattern.length() - 1);
}
if (pattern.endsWith("/**")) {
pattern = pattern.substring(0, pattern.length() - 3);
}

final String[] pattenParts = tokenizePathToString(pattern, "/");
if (exclude.startsWith("!")) {
invertExcludesList.add(exclude.substring(1));
invertExcludeList.add(pattenParts);
} else {
excludesList.add(exclude);
excludeList.add(pattenParts);
}
}
final String[] excludes = excludesList.toArray(new String[0]);
final String[] invertExcludes = invertExcludesList.toArray(new String[0]);

final List<PathMatcher> folderExcludes = buildFolderPathMaters(excludes);
final List<PathMatcher> folderInvertExcludes = buildFolderPathMaters(invertExcludes);
final List<PathMatcher> includedPatterns = buildPathMatchers(included);
final List<PathMatcher> excludedPatterns = buildPathMatchers(excludes);
final List<PathMatcher> invertExcludedPatterns = buildPathMatchers(invertExcludes);
for (String include : included) {
includeList.add(tokenizePathToString(include, "/"));
}

final List<String> results = new ArrayList<>();
final Set<FileVisitOption> followLinksOption = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
Files.walkFileTree(basePath, followLinksOption, Integer.MAX_VALUE, new FileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
final Path path = basePath.relativize(dir);
final boolean isExcluded = folderExcludes.stream().anyMatch(m -> m.matches(path));
final boolean isInvertExcluded = folderInvertExcludes.stream().anyMatch(m -> m.matches(path));
final boolean isExcluded = excludeList.stream().anyMatch(m -> matchPathPattern(m, path));
final boolean isInvertExcluded = invertExcludeList.stream().anyMatch(m -> matchPathPattern(m, path));
return (isExcluded && !isInvertExcluded) ? FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
final Path path = basePath.relativize(file);
final boolean isIncluded = includedPatterns.stream().anyMatch(m -> m.matches(path));
final boolean isExcluded = excludedPatterns.stream().anyMatch(m -> m.matches(path));
final boolean isInvertExcluded = invertExcludedPatterns.stream().anyMatch(m -> m.matches(path));
final boolean isIncluded = includeList.stream().anyMatch(m -> matchPathPattern(m, path));
final boolean isExcluded = excludeList.stream().anyMatch(m -> matchPathPattern(m, path));
final boolean isInvertExcluded = invertExcludeList.stream().anyMatch(m -> matchPathPattern(m, path));
if (isIncluded && !(isExcluded && !isInvertExcluded)) {
results.add(path.toString());
}
Expand All @@ -134,27 +144,252 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
return files;
}

private List<PathMatcher> buildFolderPathMaters(String[] patterns) {
final List<String> result = new ArrayList<>();
for (final String pattern : patterns) {
if (pattern.endsWith("/")) {
result.add(pattern.substring(0, pattern.length() - 1));
private static String[] tokenizePathToString(String path, String separator) {
final List<String> ret = new ArrayList<>();
final StringTokenizer st = new StringTokenizer(path, separator);
while (st.hasMoreTokens()) {
ret.add(st.nextToken());
}
return ret.toArray(new String[0]);
}

private static boolean matchPathPattern(String[] patDirs, Path path) {
final String[] strDirs = tokenizePathToString(path.toString(), File.separator);
return matchPathPattern(patDirs, strDirs);
}

private static boolean matchPathPattern(String[] patDirs, String[] strDirs) {
int patIdxStart = 0;
int patIdxEnd = patDirs.length - 1;
int strIdxStart = 0;
int strIdxEnd = strDirs.length - 1;

// up to first '**'
while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
String patDir = patDirs[patIdxStart];
if (patDir.equals("**")) {
break;
}
if (!match(patDir, strDirs[strIdxStart])) {
return false;
}
patIdxStart++;
strIdxStart++;
}
if (strIdxStart > strIdxEnd) {
// String is exhausted
for (int i = patIdxStart; i <= patIdxEnd; i++) {
if (!patDirs[i].equals("**")) {
return false;
}
}
return true;
} else {
if (patIdxStart > patIdxEnd) {
// String not exhausted, but pattern is. Failure.
return false;
}
}

// up to last '**'
while (patIdxStart <= patIdxEnd && strIdxStart <= strIdxEnd) {
String patDir = patDirs[patIdxEnd];
if (patDir.equals("**")) {
break;
}
if (!match(patDir, strDirs[strIdxEnd])) {
return false;
}
patIdxEnd--;
strIdxEnd--;
}
if (strIdxStart > strIdxEnd) {
// String is exhausted
for (int i = patIdxStart; i <= patIdxEnd; i++) {
if (!patDirs[i].equals("**")) {
return false;
}
}
return true;
}

while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
int patIdxTmp = -1;
for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
if (patDirs[i].equals("**")) {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == patIdxStart + 1) {
// '**/**' situation, so skip one
patIdxStart++;
continue;
}
if (pattern.endsWith("/**")) {
result.add(pattern.substring(0, pattern.length() - 3));
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = (patIdxTmp - patIdxStart - 1);
int strLength = (strIdxEnd - strIdxStart + 1);
int foundIdx = -1;
strLoop:
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
String subPat = patDirs[patIdxStart + j + 1];
String subStr = strDirs[strIdxStart + i + j];
if (!match(subPat, subStr)) {
continue strLoop;
}
}

foundIdx = strIdxStart + i;
break;
}
result.add(pattern);

if (foundIdx == -1) {
return false;
}

patIdxStart = patIdxTmp;
strIdxStart = foundIdx + patLength;
}
return buildPathMatchers(result.toArray(new String[0]));

for (int i = patIdxStart; i <= patIdxEnd; i++) {
if (!patDirs[i].equals("**")) {
return false;
}
}

return true;
}

private List<PathMatcher> buildPathMatchers(String[] patterns) {
// Patch doublestar to match zero or more segments;
// by default, doublestar in PathMatcher match one or more segments.
return stream(patterns)
.map(p -> p.replaceAll("(?=[^/])\\*\\*/", "{,**/}"))
.map(p -> fs.getPathMatcher("glob:" + p))
.toList();
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean match(String pattern, String str) {
char[] patArr = pattern.toCharArray();
char[] strArr = str.toCharArray();
int patIdxStart = 0;
int patIdxEnd = patArr.length - 1;
int strIdxStart = 0;
int strIdxEnd = strArr.length - 1;
char ch;

boolean containsStar = false;
for (char aPatArr : patArr) {
if (aPatArr == '*') {
containsStar = true;
break;
}
}

if (!containsStar) {
// No '*'s, so we make a shortcut
if (patIdxEnd != strIdxEnd) {
return false; // Pattern and string do not have the same size
}
for (int i = 0; i <= patIdxEnd; i++) {
ch = patArr[i];
if (ch != '?' && ch != strArr[i]) {
return false; // Character mismatch
}
}
return true; // String matches against pattern
}

if (patIdxEnd == 0) {
return true; // Pattern contains only '*', which matches anything
}

// Process characters before first star
// CHECKSTYLE_OFF: InnerAssignment
while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd)
// CHECKSTYLE_ON: InnerAssignment
{
if (ch != '?' && ch != strArr[strIdxStart]) {
return false; // Character mismatch
}
patIdxStart++;
strIdxStart++;
}
if (strIdxStart > strIdxEnd) {
// All characters in the string are used. Check if only '*'s are
// left in the pattern. If so, we succeeded; otherwise, we failed.
for (int i = patIdxStart; i <= patIdxEnd; i++) {
if (patArr[i] != '*') {
return false;
}
}
return true;
}

// Process characters after last star
// CHECKSTYLE_OFF: InnerAssignment
while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd)
// CHECKSTYLE_ON: InnerAssignment
{
if (ch != '?' && ch != strArr[strIdxEnd]) {
return false; // Character mismatch
}
patIdxEnd--;
strIdxEnd--;
}
if (strIdxStart > strIdxEnd) {
// All characters in the string are used. Check if only '*'s are
// left in the pattern. If so, we succeeded; otherwise, we failed.
for (int i = patIdxStart; i <= patIdxEnd; i++) {
if (patArr[i] != '*') {
return false;
}
}
return true;
}

// process pattern between stars. padIdxStart and patIdxEnd point
// always to a '*'.
while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
int patIdxTmp = -1;
for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
if (patArr[i] == '*') {
patIdxTmp = i;
break;
}
}
if (patIdxTmp == patIdxStart + 1) {
// Two stars next to each other, skip the first one.
patIdxStart++;
continue;
}
// Find the pattern between padIdxStart & padIdxTmp in str between
// strIdxStart & strIdxEnd
int patLength = (patIdxTmp - patIdxStart - 1);
int strLength = (strIdxEnd - strIdxStart + 1);
int foundIdx = -1;
strLoop:
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
ch = patArr[patIdxStart + j + 1];
if (ch != '?' && ch != strArr[strIdxStart + i + j]) {
continue strLoop;
}
}

foundIdx = strIdxStart + i;
break;
}

if (foundIdx == -1) {
return false;
}

patIdxStart = patIdxTmp;
strIdxStart = foundIdx + patLength;
}

// All characters in the string are used. Check if only '*'s are left
// in the pattern. If so, we succeeded; otherwise, we failed.
for (int i = patIdxStart; i <= patIdxEnd; i++) {
if (patArr[i] != '*') {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,19 @@ void testLimitInclusionAndCheckDefaultExcludes() {

@Test
void testExclusions(@TempDir Path tempDir) throws IOException {
final File root = createFakeProject(tempDir);
final File root = createFakeProject(tempDir, new String[]{
"included.txt",
"ignore/ignore.txt",
"target/ignored.txt",
"module/src/main/java/not-ignored.txt",
"module/target/ignored.txt",
"module/sub/subsub/src/main/java/not-ignored.txt",
"module/sub/subsub/target/foo/not-ignored.txt",
});
final Selection selection = new Selection(
root,
new String[]{"**/*.txt"},
new String[]{"target/**", "module/**/target/**"},
new String[]{"ignore", "target/**", "module/**/target/**"},
false);
final String[] selectedFiles = selection.getSelectedFiles();
final List<String> expectedSelectedFiles = List.of(
Expand All @@ -70,14 +78,11 @@ void testExclusions(@TempDir Path tempDir) throws IOException {
assertThat(selectedFiles).containsExactlyInAnyOrderElementsOf(expectedSelectedFiles);
}

private File createFakeProject(Path tempDir) throws IOException {
File temp = tempDir.toFile();
FileUtils.touch(new File(temp, "included.txt"));
FileUtils.touch(new File(temp, "target/ignored.txt"));
FileUtils.touch(new File(temp, "module/src/main/java/not-ignored.txt"));
FileUtils.touch(new File(temp, "module/target/ignored.txt"));
FileUtils.touch(new File(temp, "module/sub/subsub/src/main/java/not-ignored.txt"));
FileUtils.touch(new File(temp, "module/sub/subsub/target/foo/not-ignored.txt"));
private File createFakeProject(Path tempDir, String[] paths) throws IOException {
final File temp = tempDir.toFile();
for (String path : paths) {
FileUtils.touch(new File(temp, path));
}
return temp;
}
}