use std::fs;
use std::time::Instant;
use crate::ktlint::java_glob::PathMatcher;
use crate::ktlint::jpath::{JPath, ON_WINDOWS};
use crate::ktlint::logger::{FILE_UTILS, Logger};
const NEGATION_PREFIX: &str = "!";
const DEFAULT_KOTLIN_FILE_EXTENSIONS: [&str; 2] = ["kt", "kts"];
pub const DEFAULT_PATTERNS: [&str; 2] = ["**/*.kt", "**/*.kts"];
pub fn file_sequence(patterns: &[String], root_dir: &JPath, user_home: &str, logger: &Logger) -> Result<Vec<JPath>, String> {
if patterns.is_empty() {
logger.trace(FILE_UTILS, || "No patterns provided. Will not expand any globs.".to_owned());
return Ok(Vec::new());
}
let mut result: Vec<JPath> = Vec::new();
let (existing_files, patterns_exclusive_existing_files): (Vec<&String>, Vec<&String>) = patterns
.iter()
.partition(|p| root_dir.resolve(p).is_some_and(|path| path.to_path_buf().is_file()));
result.extend(existing_files.iter().filter_map(|p| root_dir.resolve(p)));
if !result.is_empty() && patterns_exclusive_existing_files.is_empty() {
return Ok(result);
}
let globs = expand(&patterns_exclusive_existing_files, root_dir, user_home, logger);
let negated_path_matchers = globs
.iter()
.filter_map(|g| g.strip_prefix(NEGATION_PREFIX))
.map(|g| get_path_matcher(g))
.collect::<Result<Vec<_>, _>>()?;
let mut include_globs: Vec<String> = globs.iter().filter(|g| !g.starts_with(NEGATION_PREFIX)).cloned().collect();
if !negated_path_matchers.is_empty() && include_globs.is_empty() {
logger.info(FILE_UTILS, || {
format!(
"A negate pattern is specified without an include pattern. As default, the include patterns '{}' are used.",
java_list(&DEFAULT_PATTERNS)
)
});
let defaults: Vec<String> = DEFAULT_PATTERNS.iter().map(|p| p.to_string()).collect();
include_globs.extend(expand(&defaults.iter().collect::<Vec<_>>(), root_dir, user_home, logger));
}
let mut common_root_dir = root_dir.clone();
for pattern in patterns {
if let Some(pattern_dir) = root_dir.resolve(pattern) {
common_root_dir = find_common_parent_dir(&common_root_dir, &pattern_dir.normalize());
}
}
let path_matchers = include_globs.iter().map(|g| get_path_matcher(g)).collect::<Result<Vec<_>, _>>()?;
logger.debug(FILE_UTILS, || format!("Start walkFileTree from directory: '{common_root_dir}'"));
let start = Instant::now();
walk_file_tree(&common_root_dir, &common_root_dir, &mut |path| {
if !negated_path_matchers.iter().any(|m| m.matches(path)) && path_matchers.iter().any(|m| m.matches(path)) {
logger.trace(FILE_UTILS, || format!("- File: {path}: Include as it matches patterns"));
if let Some(path) = JPath::parse(path) {
result.push(path);
}
} else {
logger.trace(FILE_UTILS, || format!("- File: {path}: Ignore"));
}
});
let duration = start.elapsed().as_millis();
logger.debug(FILE_UTILS, || format!("Discovered {} files to be processed in {duration} ms", result.len()));
Ok(result)
}
fn walk_file_tree(dir: &JPath, start: &JPath, visit_file: &mut dyn FnMut(&str)) {
let path = dir.to_path_buf();
let Ok(metadata) = fs::symlink_metadata(&path) else { return };
if !metadata.is_dir() {
visit_file(&dir.to_string());
return;
}
if dir != start && is_hidden(dir, &metadata) {
return;
}
let Ok(entries) = fs::read_dir(&path) else { return };
for entry in entries.flatten() {
if let Some(child) = dir.resolve(&entry.file_name().to_string_lossy()) {
walk_file_tree(&child, start, visit_file);
}
}
}
#[cfg(windows)]
fn is_hidden(_: &JPath, metadata: &fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt;
metadata.file_attributes() & 0x2 != 0
}
#[cfg(not(windows))]
fn is_hidden(dir: &JPath, _: &fs::Metadata) -> bool {
dir.file_name().is_some_and(|n| n.starts_with('.'))
}
fn get_path_matcher(glob: &str) -> Result<PathMatcher, String> {
let glob = glob.strip_prefix("glob:").unwrap_or(glob);
PathMatcher::new(glob, ON_WINDOWS)
}
fn find_common_parent_dir(this: &JPath, path: &JPath) -> JPath {
if path.starts_with(this) {
this.clone()
} else if this.starts_with(path) {
path.clone()
} else {
match path.parent() {
Some(parent) => find_common_parent_dir(this, &parent),
None => this.clone(),
}
}
}
fn expand(patterns: &[&String], root_dir: &JPath, user_home: &str, logger: &Logger) -> Vec<String> {
patterns
.iter()
.filter_map(|p| if ON_WINDOWS { normalize_windows_pattern(p, logger) } else { Some(p.to_string()) })
.map(|p| expand_tilde_to_full_path(&p, user_home))
.map(|p| if ON_WINDOWS { p.replace('\\', "/") } else { p })
.flat_map(|p| to_glob(&p, root_dir))
.collect()
}
fn to_glob(path: &str, root_dir: &JPath) -> Vec<String> {
let negation = if path.starts_with(NEGATION_PREFIX) { NEGATION_PREFIX } else { "" };
let path_without_negation_prefix = path.strip_prefix(NEGATION_PREFIX).unwrap_or(path);
let expanded_patterns = match root_dir.resolve(path_without_negation_prefix) {
Some(resolved) => {
let resolved_path = resolved.normalize();
if resolved_path.to_path_buf().is_dir() {
expand_path_to_default_patterns(&resolved_path)
} else {
expand_double_star_patterns(&resolved_path.to_string())
}
}
None if ON_WINDOWS => expand_double_star_patterns(path_without_negation_prefix),
None => Vec::new(),
};
expanded_patterns
.into_iter()
.map(|original| {
if ON_WINDOWS {
let p = original.replace('\\', "/");
let p = p.split_once(':').map_or(p.as_str(), |(_, after)| after);
let p = p.strip_prefix('/').unwrap_or(p);
if p.starts_with("**/") { p.to_owned() } else { format!("**/{p}") }
} else {
original
}
})
.map(|p| format!("{negation}glob:{p}"))
.collect()
}
fn expand_double_star_patterns(path: &str) -> Vec<String> {
let mut paths = vec![path.to_owned()];
let parts: Vec<&str> = path.split('/').collect();
for (i, part) in parts.iter().enumerate() {
if *part != "**" || i == parts.len() - 1 {
continue;
}
let expanded: Vec<&str> = parts.iter().enumerate().filter(|(j, _)| *j != i).map(|(_, p)| *p).collect();
for p in expand_double_star_patterns(&expanded.join("/")) {
if !paths.contains(&p) {
paths.push(p);
}
}
}
paths
}
fn normalize_windows_pattern(pattern: &str, logger: &Logger) -> Option<String> {
let mut parts: Vec<&str> = Vec::new();
let replaced = pattern.replace('\\', "/");
for part in replaced.split('/').filter(|p| *p != ".") {
if part == ".." {
match parts.last() {
None => {
logger.warn(FILE_UTILS, || {
format!("On WindowsOS the pattern '{pattern}' can not be used as it refers to a path outside of the current directory")
});
return None;
}
Some(last) if last.contains('*') => {
logger.warn(FILE_UTILS, || {
format!("On WindowsOS the pattern '{pattern}' can not be used as '/..' follows the wildcard pattern {last}")
});
return None;
}
Some(_) => {
parts.pop();
}
}
} else {
parts.push(part);
}
}
Some(parts.join("/"))
}
fn expand_path_to_default_patterns(path: &JPath) -> Vec<String> {
DEFAULT_KOTLIN_FILE_EXTENSIONS.iter().flat_map(|ext| [format!("{path}/*.{ext}"), format!("{path}/**/*.{ext}")]).collect()
}
pub fn expand_tilde_to_full_path(path: &str, user_home: &str) -> String {
if ON_WINDOWS {
return path.to_owned();
}
match path.strip_prefix("!~") {
Some(rest) => format!("{user_home}{rest}"),
None => match path.strip_prefix('~') {
Some(rest) => format!("{user_home}{rest}"),
None => path.to_owned(),
},
}
}
pub fn location(path: &JPath, relative: bool, root_dir_path: &JPath) -> String {
if relative { path.relative_to_or_self(root_dir_path).to_string() } else { path.to_string() }
}
pub fn java_list<S: AsRef<str>>(items: &[S]) -> String {
let items: Vec<&str> = items.iter().map(AsRef::as_ref).collect();
format!("[{}]", items.join(", "))
}