CleanupInputCollector.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/
package org.sandbox.jdt.core.cleanupapp;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.eclipse.jdt.internal.core.util.Util;

/**
 * Expands command-line files and directories into one deterministic, canonical
 * Java source selection without modifying workspace or file contents.
 */
final class CleanupInputCollector {

	private static final Set<String> TEST_SEGMENTS= Set.of(
			"test", "tests", "testfixtures", "integrationtest", "integration-test"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$

	/** One input that could not be inspected safely. */
	record Diagnostic(String input, String message) {
		Diagnostic {
			input= input == null ? "" : input; //$NON-NLS-1$
			message= message == null ? "" : message; //$NON-NLS-1$
		}
	}

	/** Stable result of the discovery phase. */
	record Result(List<File> files, List<Diagnostic> diagnostics) {
		Result {
			files= List.copyOf(files);
			diagnostics= List.copyOf(diagnostics);
		}
	}

	Result collect(Collection<File> inputs, CodeCleanupApplication.CleanupScope scope) {
		if (scope == null) {
			throw new NullPointerException("scope"); //$NON-NLS-1$
		}
		Map<String, File> filesByCanonicalPath= new LinkedHashMap<>();
		List<Diagnostic> diagnostics= new ArrayList<>();
		if (inputs == null) {
			return new Result(List.of(), List.of(new Diagnostic("", "Input collection is null"))); //$NON-NLS-1$ //$NON-NLS-2$
		}
		for (File input : inputs) {
			if (input == null) {
				diagnostics.add(new Diagnostic("", "Input entry is null")); //$NON-NLS-1$ //$NON-NLS-2$
				continue;
			}
			Path root;
			try {
				root= input.toPath().toRealPath();
			} catch (IOException exception) {
				diagnostics.add(new Diagnostic(input.getPath(), exception.getMessage()));
				continue;
			}
			if (Files.isDirectory(root)) {
				collectDirectory(root, scope, filesByCanonicalPath, diagnostics);
			} else {
				addCandidate(root, scope, filesByCanonicalPath, diagnostics);
			}
		}
		List<File> files= new ArrayList<>(filesByCanonicalPath.values());
		files.sort(Comparator.comparing(file -> stablePath(file.toPath())));
		diagnostics.sort(Comparator.comparing(Diagnostic::input).thenComparing(Diagnostic::message));
		return new Result(files, diagnostics);
	}

	private static void collectDirectory(Path root, CodeCleanupApplication.CleanupScope scope,
			Map<String, File> filesByCanonicalPath, List<Diagnostic> diagnostics) {
		try (Stream<Path> paths= Files.walk(root)) {
			paths.filter(Files::isRegularFile)
					.sorted(Comparator.comparing(CleanupInputCollector::stablePath))
					.forEach(path -> addCandidate(path, scope, filesByCanonicalPath, diagnostics));
		} catch (IOException | UncheckedIOException exception) {
			diagnostics.add(new Diagnostic(stablePath(root), exception.getMessage()));
		}
	}

	private static void addCandidate(Path path, CodeCleanupApplication.CleanupScope scope,
			Map<String, File> filesByCanonicalPath, List<Diagnostic> diagnostics) {
		if (!Util.isJavaLikeFileName(path.toString()) || !includes(scope, path)) {
			return;
		}
		try {
			Path canonical= path.toRealPath();
			filesByCanonicalPath.putIfAbsent(stablePath(canonical), canonical.toFile());
		} catch (IOException exception) {
			diagnostics.add(new Diagnostic(stablePath(path), exception.getMessage()));
		}
	}

	private static boolean includes(CodeCleanupApplication.CleanupScope scope, Path path) {
		boolean testSource= isTestPath(path);
		return switch (scope) {
			case BOTH -> true;
			case MAIN -> !testSource;
			case TEST -> testSource;
		};
	}

	private static String stablePath(Path path) {
		return path.toAbsolutePath().normalize().toString().replace(File.separatorChar, '/');
	}

	static boolean isTestPath(Path path) {
		Path normalized= path.normalize();
		int lastSourceSegment= -1;
		for (int index= 0; index < normalized.getNameCount(); index++) {
			String value= normalized.getName(index).toString().toLowerCase(Locale.ROOT);
			if ("src".equals(value)) { //$NON-NLS-1$
				lastSourceSegment= index;
			}
		}
		if (lastSourceSegment >= 0) {
			if (lastSourceSegment + 1 >= normalized.getNameCount()) {
				return false;
			}
			String sourceSet= normalized.getName(lastSourceSegment + 1).toString().toLowerCase(Locale.ROOT);
			return TEST_SEGMENTS.contains(sourceSet);
		}
		for (Path segment : normalized) {
			if (TEST_SEGMENTS.contains(segment.toString().toLowerCase(Locale.ROOT))) {
				return true;
			}
		}
		return false;
	}
}