1use std::{collections::HashSet, path::PathBuf};
2
3use dotenv_analyzer::LintKind;
4use dotenv_schema::DotEnvSchema;
5
6use crate::{
7 diff::{DiffFileType, DiffWarning},
8 output::{check::CheckOutput, diff::DiffOutput, fix::FixOutput},
9};
10
11mod fs_utils;
12
13pub mod cli;
14mod diff;
15mod output;
16
17pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
18
19pub struct CheckOptions<'a> {
20 pub files: Vec<&'a PathBuf>,
21 pub ignore_checks: Vec<LintKind>,
22 pub exclude: Vec<&'a PathBuf>,
23 pub quiet: bool,
24 pub recursive: bool,
25 pub schema: Option<DotEnvSchema>,
26}
27
28pub fn check(opts: &CheckOptions, current_dir: &PathBuf) -> Result<usize> {
29 let files = dotenv_finder::FinderBuilder::new(current_dir)
30 .with_paths(&opts.files)
31 .exclude(&opts.exclude)
32 .recursive(opts.recursive)
33 .build()
34 .find();
35
36 let output = CheckOutput::new(opts.quiet);
37
38 if files.is_empty() {
39 output.print_nothing_to_check();
40 return Ok(0);
41 }
42
43 let output = output.files_count(files.len());
44
45 let warnings_count = files
46 .into_iter()
47 .enumerate()
48 .fold(0, |acc, (index, (fe, lines))| {
49 output.print_processing_info(&fe);
50
51 let warnings =
52 dotenv_analyzer::check(&lines, &opts.ignore_checks, opts.schema.as_ref());
53 output.print_warnings(&fe, &warnings, index);
54 acc + warnings.len()
55 });
56
57 output.print_total(warnings_count);
58 Ok(warnings_count)
59}
60
61pub struct FixOptions<'a> {
62 pub files: Vec<&'a PathBuf>,
63 pub ignore_checks: Vec<LintKind>,
64 pub exclude: Vec<&'a PathBuf>,
65 pub quiet: bool,
66 pub recursive: bool,
67 pub no_backup: bool,
68 pub dry_run: bool,
69}
70
71pub fn fix(opts: &FixOptions, current_dir: &PathBuf) -> Result<()> {
72 let files = dotenv_finder::FinderBuilder::new(current_dir)
73 .with_paths(&opts.files)
74 .exclude(&opts.exclude)
75 .recursive(opts.recursive)
76 .build()
77 .find();
78
79 let output = FixOutput::new(opts.quiet);
80
81 if files.is_empty() {
82 output.print_nothing_to_fix();
83 return Ok(());
84 }
85
86 let output = output.files_count(files.len());
87
88 let mut warnings_count = 0;
89 for (index, (fe, mut lines)) in files.into_iter().enumerate() {
90 output.print_processing_info(&fe);
91
92 let warnings = dotenv_analyzer::check(&lines, &opts.ignore_checks, None);
93 if warnings.is_empty() {
94 continue;
95 }
96
97 let fixes_done = dotenv_analyzer::fix(&warnings, &mut lines, &opts.ignore_checks);
98 if fixes_done != warnings.len() {
99 output.print_not_all_warnings_fixed();
100 }
101
102 if opts.dry_run {
103 output.print_dry_run(&lines);
104 } else if fixes_done > 0 {
105 let should_backup = !opts.no_backup;
106 if should_backup {
108 let backup_file = fs_utils::backup_file(&fe)?;
109 output.print_backup(&backup_file);
110 }
111
112 fs_utils::write_file(&fe.path, lines)?;
114 }
115
116 output.print_warnings(&fe, &warnings, index);
117 warnings_count += warnings.len();
118 }
119
120 output.print_total(warnings_count);
121 Ok(())
122}
123
124pub struct DiffOptions<'a> {
125 pub files: Vec<&'a PathBuf>,
126 pub quiet: bool,
127}
128
129pub fn diff(opts: &DiffOptions, current_dir: &PathBuf) -> Result<usize> {
131 let files = dotenv_finder::FinderBuilder::new(current_dir)
132 .with_paths(&opts.files)
133 .build()
134 .find();
135 let output = DiffOutput::new(opts.quiet);
136
137 if files.is_empty() || files.len() < 2 {
138 output.print_nothing_to_compare();
139 return Ok(0);
140 }
141
142 let mut all_keys: HashSet<String> = HashSet::new();
144 let mut files_to_compare: Vec<DiffFileType> = Vec::new();
145 for (fe, lines) in files.into_iter() {
146 output.print_processing_info(&fe);
147
148 let mut keys: Vec<String> = Vec::new();
149
150 for line in lines {
151 if let Some(key) = line.get_key() {
152 all_keys.insert(key.to_string());
153 keys.push(key.to_string());
154 }
155 }
156
157 let file_to_compare: DiffFileType = DiffFileType::new(fe.path, keys);
158
159 files_to_compare.push(file_to_compare);
160 }
161
162 let mut warnings: Vec<DiffWarning> = Vec::new();
164 for file in files_to_compare {
165 let missing_keys: Vec<_> = all_keys
166 .iter()
167 .filter(|key| !file.keys().contains(key))
168 .map(|key| key.to_owned())
169 .collect();
170
171 if !missing_keys.is_empty() {
172 let warning = DiffWarning::new(file.path().clone(), missing_keys);
173
174 warnings.push(warning)
175 }
176 }
177
178 if warnings.is_empty() {
180 output.print_no_difference_found();
181 return Ok(0);
182 }
183
184 output.print_warnings(&warnings);
185 Ok(warnings.len())
186}
187
188#[cfg(feature = "update-informer")]
190pub(crate) fn check_for_updates() {
191 use colored::*;
192 use update_informer::{Check, registry};
193
194 let pkg_name = env!("CARGO_PKG_NAME");
195 #[cfg(not(feature = "stub_check_version"))]
196 let current_version = env!("CARGO_PKG_VERSION");
197 #[cfg(feature = "stub_check_version")]
198 let current_version = "3.0.0";
199
200 #[cfg(not(feature = "stub_check_version"))]
201 let informer = update_informer::new(registry::Crates, pkg_name, current_version);
202 #[cfg(feature = "stub_check_version")]
203 let informer = update_informer::fake(registry::Crates, pkg_name, current_version, "3.1.1");
204
205 if let Ok(Some(version)) = informer.check_version() {
206 let msg = format!(
207 "A new release of {pkg_name} is available: v{current_version} -> {new_version}",
208 pkg_name = pkg_name.italic().cyan(),
209 current_version = current_version,
210 new_version = version.to_string().green()
211 );
212
213 let release_url =
214 format!("https://github.com/{pkg_name}/{pkg_name}/releases/tag/{version}").yellow();
215
216 println!("\n{msg}\n{release_url}");
217 }
218}