dotenv_linter/
lib.rs

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            // create backup copy unless user specifies not to
107            if should_backup {
108                let backup_file = fs_utils::backup_file(&fe)?;
109                output.print_backup(&backup_file);
110            }
111
112            // write corrected file
113            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
129// Compares if different environment files contains the same variables and returns warnings if not
130pub 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    // Create DiffFileType structures for each file
143    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    // Create warnings if any file misses any key
163    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    // Create success message if no warnings found.
179    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/// Checks for updates and prints information about the new version to `STDOUT`
189#[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}