1use crate::{common::*, quote_type::QuoteType};
2use clap::Values;
3use std::{
4 collections::{BTreeMap, HashSet},
5 path::{Path, PathBuf},
6 str::FromStr,
7};
8
9pub use checks::available_check_names;
10
11mod checks;
12mod common;
13mod fixes;
14mod fs_utils;
15
16pub mod cli;
17
18pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
19
20pub fn check(args: &clap::ArgMatches, current_dir: &Path) -> Result<usize> {
21 let lines_map = get_lines(
22 args,
23 current_dir,
24 args.is_present("recursive"),
25 args.values_of("exclude"),
26 );
27 let output = CheckOutput::new(args.is_present("quiet"), lines_map.len());
28
29 if lines_map.is_empty() {
30 output.print_nothing_to_check();
31
32 #[cfg(feature = "update-informer")]
33 print_new_version_if_available(args);
34
35 return Ok(0);
36 }
37
38 let skip_checks: Vec<LintKind> = args
39 .values_of("skip")
40 .unwrap_or_default()
41 .filter_map(|check: &str| LintKind::from_str(check).ok())
42 .collect();
43
44 let warnings_count =
45 lines_map
46 .into_iter()
47 .enumerate()
48 .fold(0, |acc, (index, (fe, strings))| {
49 output.print_processing_info(&fe);
50
51 let lines = get_line_entries(strings);
52 let result = checks::run(&lines, &skip_checks);
53
54 output.print_warnings(&fe, &result, index);
55 acc + result.len()
56 });
57
58 output.print_total(warnings_count);
59
60 #[cfg(feature = "update-informer")]
61 print_new_version_if_available(args);
62
63 Ok(warnings_count)
64}
65
66pub fn fix(args: &clap::ArgMatches, current_dir: &Path) -> Result<()> {
67 let mut warnings_count = 0;
68 let lines_map = get_lines(
69 args,
70 current_dir,
71 args.is_present("recursive"),
72 args.values_of("exclude"),
73 );
74 let output = FixOutput::new(args.is_present("quiet"), lines_map.len());
75
76 if lines_map.is_empty() {
78 output.print_nothing_to_fix();
79 return Ok(());
80 }
81
82 let skip_checks: Vec<LintKind> = args
83 .values_of("skip")
84 .unwrap_or_default()
85 .filter_map(|check: &str| LintKind::from_str(check).ok())
86 .collect();
87
88 for (index, (fe, strings)) in lines_map.into_iter().enumerate() {
89 output.print_processing_info(&fe);
90
91 let mut lines = get_line_entries(strings);
92 let result = checks::run(&lines, &skip_checks);
93 if result.is_empty() {
94 continue;
95 }
96 let fixes_done = fixes::run(&result, &mut lines, &skip_checks);
97 if fixes_done != result.len() {
98 output.print_not_all_warnings_fixed();
99 }
100 if fixes_done > 0 {
101 let should_backup = !args.is_present("no-backup");
102 if should_backup {
104 let backup_file = fs_utils::backup_file(&fe)?;
105 output.print_backup(&backup_file);
106 }
107
108 fs_utils::write_file(&fe.path, lines)?;
110 }
111
112 output.print_warnings(&fe, &result, index);
113 warnings_count += result.len();
114 }
115
116 output.print_total(warnings_count);
117 Ok(())
118}
119
120pub fn compare(args: &clap::ArgMatches, current_dir: &Path) -> Result<Vec<CompareWarning>> {
122 let mut all_keys: HashSet<String> = HashSet::new();
123 let lines_map = get_lines(args, current_dir, false, None);
124 let output = CompareOutput::new(args.is_present("quiet"));
125
126 let mut warnings: Vec<CompareWarning> = Vec::new();
127 let mut files_to_compare: Vec<CompareFileType> = Vec::new();
128
129 if lines_map.is_empty() {
131 output.print_nothing_to_compare();
132 return Ok(warnings);
133 }
134
135 for (_, (fe, strings)) in lines_map.into_iter().enumerate() {
137 output.print_processing_info(&fe);
138 let lines = get_line_entries(strings);
139 let mut keys: Vec<String> = Vec::new();
140
141 for line in lines {
142 if let Some(key) = line.get_key() {
143 all_keys.insert(key.to_string());
144 keys.push(key.to_string());
145 }
146 }
147
148 let file_to_compare: CompareFileType = CompareFileType {
149 path: fe.path,
150 keys,
151 missing: Vec::new(),
152 };
153
154 files_to_compare.push(file_to_compare);
155 }
156
157 for file in files_to_compare {
159 let missing_keys: Vec<_> = all_keys
160 .iter()
161 .filter(|key| !file.keys.contains(key))
162 .map(|key| key.to_owned())
163 .collect();
164
165 if !missing_keys.is_empty() {
166 let warning = CompareWarning {
167 path: file.path,
168 missing_keys,
169 };
170
171 warnings.push(warning)
172 }
173 }
174
175 output.print_warnings(&warnings);
176 Ok(warnings)
177}
178
179fn get_lines(
180 args: &clap::ArgMatches,
181 current_dir: &Path,
182 is_recursive: bool,
183 exclude: Option<Values>,
184) -> BTreeMap<FileEntry, Vec<String>> {
185 let file_paths: Vec<PathBuf> = get_needed_file_paths(args, is_recursive, exclude);
186
187 file_paths
188 .iter()
189 .filter_map(|path: &PathBuf| -> Option<(FileEntry, Vec<String>)> {
190 fs_utils::get_relative_path(path, current_dir).and_then(FileEntry::from)
191 })
192 .collect()
193}
194
195fn get_needed_file_paths(
197 args: &clap::ArgMatches,
198 is_recursive: bool,
199 exclude: Option<Values>,
200) -> Vec<PathBuf> {
201 let mut file_paths: Vec<PathBuf> = Vec::new();
202 let mut excluded_paths: Vec<PathBuf> = Vec::new();
203
204 if let Some(excluded) = exclude {
205 excluded_paths = excluded
206 .filter_map(|f| fs_utils::canonicalize(f).ok())
207 .collect();
208 }
209
210 if let Some(inputs) = args.values_of("input") {
211 let input_paths = inputs
212 .filter_map(|s| fs_utils::canonicalize(s).ok())
213 .collect();
214
215 file_paths.extend(get_file_paths(input_paths, &excluded_paths, is_recursive));
216 }
217
218 file_paths
219}
220
221fn get_file_paths(
222 dir_entries: Vec<PathBuf>,
223 excludes: &[PathBuf],
224 is_recursive: bool,
225) -> Vec<PathBuf> {
226 let nested_paths: Vec<PathBuf> = dir_entries
227 .iter()
228 .filter(|entry| entry.is_dir())
229 .filter(|entry| !excludes.contains(entry))
230 .filter_map(|dir| dir.read_dir().ok())
231 .map(|read_dir| {
232 read_dir
233 .filter_map(|e| e.ok())
234 .map(|e| e.path())
235 .filter(|path| {
236 FileEntry::is_env_file(path)
237 || (is_recursive && path.is_dir() && path.read_link().is_err())
238 })
239 .collect()
240 })
241 .flat_map(|dir_entries| get_file_paths(dir_entries, excludes, is_recursive))
242 .collect();
243
244 let mut file_paths: Vec<PathBuf> = dir_entries
245 .into_iter()
246 .filter(|entry| entry.is_file())
247 .filter(|entry| !excludes.contains(entry))
248 .collect();
249
250 file_paths.extend(nested_paths);
251 file_paths.sort();
252 file_paths.dedup();
253 file_paths
254}
255
256fn get_line_entries(lines: Vec<String>) -> Vec<LineEntry> {
257 let length = lines.len();
258
259 let mut lines: Vec<LineEntry> = lines
260 .into_iter()
261 .enumerate()
262 .map(|(index, line)| LineEntry::new(index + 1, line, length == (index + 1)))
263 .collect();
264
265 reduce_multiline_entries(&mut lines);
266 lines
267}
268
269fn reduce_multiline_entries(lines: &mut Vec<LineEntry>) {
270 let length = lines.len();
271 let multiline_ranges = find_multiline_ranges(lines);
272
273 let mut offset = 1; for (start, end) in multiline_ranges {
276 let result = lines
277 .drain(start - offset..end - offset + 1) .map(|entry| entry.raw_string)
279 .reduce(|result, line| result + "\n" + &line); if let Some(value) = result {
282 lines.insert(start - offset, LineEntry::new(start, value, length == end));
283 }
284
285 offset += end - start;
286 }
287}
288
289fn find_multiline_ranges(lines: &[LineEntry]) -> Vec<(usize, usize)> {
290 let mut multiline_ranges: Vec<(usize, usize)> = Vec::new();
291 let mut start_number: Option<usize> = None;
292 let mut quote_char: Option<char> = None;
293
294 lines.iter().for_each(|entry| {
296 if let Some(start) = start_number {
297 if let Some(quote_char) = quote_char {
298 if let Some(idx) = entry.raw_string.find(quote_char) {
299 if !is_escaped(&entry.raw_string[..idx]) {
300 multiline_ranges.push((start, entry.number));
301 start_number = None;
302 }
303 }
304 }
305 } else if let Some(trimmed_value) = entry.get_value().map(|val| val.trim()) {
306 if let Some(quote_type) = is_multiline_start(trimmed_value) {
307 quote_char = Some(quote_type.char());
308 start_number = Some(entry.number);
309 }
310 }
311 });
312
313 multiline_ranges
314}
315
316fn is_multiline_start(val: &str) -> Option<QuoteType> {
318 [QuoteType::Single, QuoteType::Double]
319 .into_iter()
320 .find(|quote_type| quote_type.is_quoted_value(val))
321}
322
323#[cfg(feature = "update-informer")]
325fn print_new_version_if_available(args: &clap::ArgMatches) {
326 use colored::*;
327 use update_informer::{registry, Check};
328
329 if args.is_present("not-check-updates") || args.is_present("quiet") {
330 return;
331 }
332
333 let pkg_name = env!("CARGO_PKG_NAME");
334
335 #[cfg(not(feature = "stub_check_version"))]
336 let current_version = env!("CARGO_PKG_VERSION");
337 #[cfg(feature = "stub_check_version")]
338 let current_version = "3.0.0";
339
340 #[cfg(not(feature = "stub_check_version"))]
341 let informer = update_informer::new(registry::Crates, pkg_name, current_version);
342 #[cfg(feature = "stub_check_version")]
343 let informer = update_informer::fake(registry::Crates, pkg_name, current_version, "3.1.1");
344
345 if let Ok(Some(version)) = informer.check_version() {
346 let msg = format!(
347 "A new release of {pkg_name} is available: v{current_version} -> {new_version}",
348 pkg_name = pkg_name.italic().cyan(),
349 current_version = current_version,
350 new_version = version.to_string().green()
351 );
352
353 let release_url = format!(
354 "https://github.com/{pkg_name}/{pkg_name}/releases/tag/{version}",
355 pkg_name = pkg_name,
356 version = version
357 )
358 .yellow();
359
360 println!("\n{msg}\n{url}", msg = msg, url = release_url);
361 }
362}