ktrs_cli/ktlint/
file_utils.rs1use std::fs;
5use std::time::Instant;
6
7use crate::ktlint::java_glob::PathMatcher;
8use crate::ktlint::jpath::{JPath, ON_WINDOWS};
9use crate::ktlint::logger::{FILE_UTILS, Logger};
10
11const NEGATION_PREFIX: &str = "!";
12const DEFAULT_KOTLIN_FILE_EXTENSIONS: [&str; 2] = ["kt", "kts"];
13pub const DEFAULT_PATTERNS: [&str; 2] = ["**/*.kt", "**/*.kts"];
14
15pub fn file_sequence(patterns: &[String], root_dir: &JPath, user_home: &str, logger: &Logger) -> Result<Vec<JPath>, String> {
18 if patterns.is_empty() {
19 logger.trace(FILE_UTILS, || "No patterns provided. Will not expand any globs.".to_owned());
20 return Ok(Vec::new());
21 }
22 let mut result: Vec<JPath> = Vec::new();
23 let (existing_files, patterns_exclusive_existing_files): (Vec<&String>, Vec<&String>) = patterns
24 .iter()
25 .partition(|p| root_dir.resolve(p).is_some_and(|path| path.to_path_buf().is_file()));
26 result.extend(existing_files.iter().filter_map(|p| root_dir.resolve(p)));
27 if !result.is_empty() && patterns_exclusive_existing_files.is_empty() {
28 return Ok(result);
29 }
30
31 let globs = expand(&patterns_exclusive_existing_files, root_dir, user_home, logger);
32 let negated_path_matchers = globs
33 .iter()
34 .filter_map(|g| g.strip_prefix(NEGATION_PREFIX))
35 .map(|g| get_path_matcher(g))
36 .collect::<Result<Vec<_>, _>>()?;
37 let mut include_globs: Vec<String> = globs.iter().filter(|g| !g.starts_with(NEGATION_PREFIX)).cloned().collect();
38 if !negated_path_matchers.is_empty() && include_globs.is_empty() {
39 logger.info(FILE_UTILS, || {
40 format!(
41 "A negate pattern is specified without an include pattern. As default, the include patterns '{}' are used.",
42 java_list(&DEFAULT_PATTERNS)
43 )
44 });
45 let defaults: Vec<String> = DEFAULT_PATTERNS.iter().map(|p| p.to_string()).collect();
46 include_globs.extend(expand(&defaults.iter().collect::<Vec<_>>(), root_dir, user_home, logger));
47 }
48 let mut common_root_dir = root_dir.clone();
49 for pattern in patterns {
50 if let Some(pattern_dir) = root_dir.resolve(pattern) {
51 common_root_dir = find_common_parent_dir(&common_root_dir, &pattern_dir.normalize());
52 }
53 }
54 let path_matchers = include_globs.iter().map(|g| get_path_matcher(g)).collect::<Result<Vec<_>, _>>()?;
55
56 logger.debug(FILE_UTILS, || format!("Start walkFileTree from directory: '{common_root_dir}'"));
57 let start = Instant::now();
58 walk_file_tree(&common_root_dir, &common_root_dir, &mut |path| {
59 if !negated_path_matchers.iter().any(|m| m.matches(path)) && path_matchers.iter().any(|m| m.matches(path)) {
60 logger.trace(FILE_UTILS, || format!("- File: {path}: Include as it matches patterns"));
61 if let Some(path) = JPath::parse(path) {
62 result.push(path);
63 }
64 } else {
65 logger.trace(FILE_UTILS, || format!("- File: {path}: Ignore"));
66 }
67 });
68 let duration = start.elapsed().as_millis();
69 logger.debug(FILE_UTILS, || format!("Discovered {} files to be processed in {duration} ms", result.len()));
70 Ok(result)
71}
72
73fn walk_file_tree(dir: &JPath, start: &JPath, visit_file: &mut dyn FnMut(&str)) {
76 let path = dir.to_path_buf();
77 let Ok(metadata) = fs::symlink_metadata(&path) else { return };
78 if !metadata.is_dir() {
79 visit_file(&dir.to_string());
80 return;
81 }
82 if dir != start && is_hidden(dir, &metadata) {
83 return;
84 }
85 let Ok(entries) = fs::read_dir(&path) else { return };
86 for entry in entries.flatten() {
87 if let Some(child) = dir.resolve(&entry.file_name().to_string_lossy()) {
88 walk_file_tree(&child, start, visit_file);
89 }
90 }
91}
92
93#[cfg(windows)]
94fn is_hidden(_: &JPath, metadata: &fs::Metadata) -> bool {
95 use std::os::windows::fs::MetadataExt;
96 metadata.file_attributes() & 0x2 != 0
97}
98
99#[cfg(not(windows))]
100fn is_hidden(dir: &JPath, _: &fs::Metadata) -> bool {
101 dir.file_name().is_some_and(|n| n.starts_with('.'))
102}
103
104fn get_path_matcher(glob: &str) -> Result<PathMatcher, String> {
105 let glob = glob.strip_prefix("glob:").unwrap_or(glob);
106 PathMatcher::new(glob, ON_WINDOWS)
107}
108
109fn find_common_parent_dir(this: &JPath, path: &JPath) -> JPath {
110 if path.starts_with(this) {
111 this.clone()
112 } else if this.starts_with(path) {
113 path.clone()
114 } else {
115 match path.parent() {
116 Some(parent) => find_common_parent_dir(this, &parent),
117 None => this.clone(),
118 }
119 }
120}
121
122fn expand(patterns: &[&String], root_dir: &JPath, user_home: &str, logger: &Logger) -> Vec<String> {
123 patterns
124 .iter()
125 .filter_map(|p| if ON_WINDOWS { normalize_windows_pattern(p, logger) } else { Some(p.to_string()) })
126 .map(|p| expand_tilde_to_full_path(&p, user_home))
127 .map(|p| if ON_WINDOWS { p.replace('\\', "/") } else { p })
128 .flat_map(|p| to_glob(&p, root_dir))
129 .collect()
130}
131
132fn to_glob(path: &str, root_dir: &JPath) -> Vec<String> {
133 let negation = if path.starts_with(NEGATION_PREFIX) { NEGATION_PREFIX } else { "" };
134 let path_without_negation_prefix = path.strip_prefix(NEGATION_PREFIX).unwrap_or(path);
135 let expanded_patterns = match root_dir.resolve(path_without_negation_prefix) {
136 Some(resolved) => {
137 let resolved_path = resolved.normalize();
138 if resolved_path.to_path_buf().is_dir() {
139 expand_path_to_default_patterns(&resolved_path)
140 } else {
141 expand_double_star_patterns(&resolved_path.to_string())
142 }
143 }
144 None if ON_WINDOWS => expand_double_star_patterns(path_without_negation_prefix),
145 None => Vec::new(),
146 };
147 expanded_patterns
148 .into_iter()
149 .map(|original| {
150 if ON_WINDOWS {
151 let p = original.replace('\\', "/");
152 let p = p.split_once(':').map_or(p.as_str(), |(_, after)| after);
153 let p = p.strip_prefix('/').unwrap_or(p);
154 if p.starts_with("**/") { p.to_owned() } else { format!("**/{p}") }
155 } else {
156 original
157 }
158 })
159 .map(|p| format!("{negation}glob:{p}"))
160 .collect()
161}
162
163fn expand_double_star_patterns(path: &str) -> Vec<String> {
165 let mut paths = vec![path.to_owned()];
166 let parts: Vec<&str> = path.split('/').collect();
167 for (i, part) in parts.iter().enumerate() {
168 if *part != "**" || i == parts.len() - 1 {
169 continue;
170 }
171 let expanded: Vec<&str> = parts.iter().enumerate().filter(|(j, _)| *j != i).map(|(_, p)| *p).collect();
172 for p in expand_double_star_patterns(&expanded.join("/")) {
173 if !paths.contains(&p) {
174 paths.push(p);
175 }
176 }
177 }
178 paths
179}
180
181fn normalize_windows_pattern(pattern: &str, logger: &Logger) -> Option<String> {
182 let mut parts: Vec<&str> = Vec::new();
183 let replaced = pattern.replace('\\', "/");
184 for part in replaced.split('/').filter(|p| *p != ".") {
185 if part == ".." {
186 match parts.last() {
187 None => {
188 logger.warn(FILE_UTILS, || {
189 format!("On WindowsOS the pattern '{pattern}' can not be used as it refers to a path outside of the current directory")
190 });
191 return None;
192 }
193 Some(last) if last.contains('*') => {
194 logger.warn(FILE_UTILS, || {
195 format!("On WindowsOS the pattern '{pattern}' can not be used as '/..' follows the wildcard pattern {last}")
196 });
197 return None;
198 }
199 Some(_) => {
200 parts.pop();
201 }
202 }
203 } else {
204 parts.push(part);
205 }
206 }
207 Some(parts.join("/"))
208}
209
210fn expand_path_to_default_patterns(path: &JPath) -> Vec<String> {
211 DEFAULT_KOTLIN_FILE_EXTENSIONS.iter().flat_map(|ext| [format!("{path}/*.{ext}"), format!("{path}/**/*.{ext}")]).collect()
212}
213
214pub fn expand_tilde_to_full_path(path: &str, user_home: &str) -> String {
217 if ON_WINDOWS {
218 return path.to_owned();
219 }
220 match path.strip_prefix("!~") {
221 Some(rest) => format!("{user_home}{rest}"),
222 None => match path.strip_prefix('~') {
223 Some(rest) => format!("{user_home}{rest}"),
224 None => path.to_owned(),
225 },
226 }
227}
228
229pub fn location(path: &JPath, relative: bool, root_dir_path: &JPath) -> String {
231 if relative { path.relative_to_or_self(root_dir_path).to_string() } else { path.to_string() }
232}
233
234pub fn java_list<S: AsRef<str>>(items: &[S]) -> String {
236 let items: Vec<&str> = items.iter().map(AsRef::as_ref).collect();
237 format!("[{}]", items.join(", "))
238}