1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
use crate::common::*;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::error::Error;
use std::path::{Path, PathBuf};
use std::str::FromStr;

mod checks;
pub mod cli;
mod common;
mod fixes;
mod fs_utils;
mod lint_kind;

pub use checks::available_check_names;
use common::CompareWarning;
use lint_kind::LintKind;
use std::rc::Rc;

pub fn check(args: &clap::ArgMatches, current_dir: &Path) -> Result<usize, Box<dyn Error>> {
    let lines_map = get_lines(args, current_dir);
    let output = CheckOutput::new(args.is_present("quiet"), lines_map.len());

    if lines_map.is_empty() {
        output.print_nothing_to_check();
        return Ok(0);
    }

    let mut skip_checks: Vec<&str> = Vec::new();
    if let Some(skip) = args.values_of("skip") {
        skip_checks = skip.collect();
    }

    let skip_checks = skip_checks
        .into_iter()
        .filter_map(|c| LintKind::from_str(c).ok())
        .collect::<Vec<LintKind>>();

    let warnings_count =
        lines_map
            .into_iter()
            .enumerate()
            .fold(0, |acc, (index, (fe, strings))| {
                output.print_processing_info(&fe);

                let lines = get_line_entries(&fe, strings);
                let result = checks::run(&lines, &skip_checks);

                output.print_warnings(&result, index);
                acc + result.len()
            });

    output.print_total(warnings_count);

    Ok(warnings_count)
}

pub fn fix(args: &clap::ArgMatches, current_dir: &Path) -> Result<(), Box<dyn Error>> {
    let mut warnings_count = 0;
    let lines_map = get_lines(args, current_dir);
    let output = FixOutput::new(args.is_present("quiet"), lines_map.len());

    // Nothing to fix
    if lines_map.is_empty() {
        output.print_nothing_to_fix();
        return Ok(());
    }

    let mut skip_checks: Vec<&str> = Vec::new();
    if let Some(skip) = args.values_of("skip") {
        skip_checks = skip.collect();
    }
    let skip_checks = skip_checks
        .into_iter()
        .filter_map(|c| LintKind::from_str(c).ok())
        .collect::<Vec<LintKind>>();

    for (index, (fe, strings)) in lines_map.into_iter().enumerate() {
        output.print_processing_info(&fe);

        let mut lines = get_line_entries(&fe, strings);
        let mut result = checks::run(&lines, &skip_checks);
        if result.is_empty() {
            continue;
        }
        let fixes_done = fixes::run(&mut result, &mut lines, &skip_checks);
        if fixes_done != result.len() {
            output.print_not_all_warnings_fixed();
        }
        if fixes_done > 0 {
            let should_backup = !args.is_present("no-backup");
            // create backup copy unless user specifies not to
            if should_backup {
                let backup_file = fs_utils::backup_file(&fe)?.into_os_string();
                output.print_backup(&backup_file);
            }

            // write corrected file
            fs_utils::write_file(&fe.path, lines)?;
        }

        output.print_warnings(&result, index);
        warnings_count += result.len();
    }

    output.print_total(warnings_count);

    Ok(())
}

// Compares if different environment files contains the same variables and returns warnings if not
pub fn compare(
    args: &clap::ArgMatches,
    current_dir: &Path,
) -> Result<Vec<CompareWarning>, Box<dyn Error>> {
    let mut all_keys: HashSet<String> = HashSet::new();
    let lines_map = get_lines(args, current_dir);
    let output = CompareOutput::new(args.is_present("quiet"));

    let mut warnings: Vec<CompareWarning> = Vec::new();
    let mut files_to_compare: Vec<CompareFileType> = Vec::new();

    // Nothing to check
    if lines_map.is_empty() {
        output.print_nothing_to_compare();
        return Ok(warnings);
    }

    // Create CompareFileType structures for each file
    for (_, (fe, strings)) in lines_map.into_iter().enumerate() {
        output.print_processing_info(&fe);
        let lines = get_line_entries(&fe, strings);
        let mut keys: Vec<String> = Vec::new();

        for line in lines {
            if let Some(key) = line.get_key() {
                all_keys.insert(key.to_string());
                keys.push(key.to_string());
            }
        }

        let file_to_compare: CompareFileType = CompareFileType {
            path: fe.path,
            keys,
            missing: Vec::new(),
        };

        files_to_compare.push(file_to_compare);
    }

    // Create warnings if any file misses any key
    for file in files_to_compare {
        let missing_keys: Vec<_> = all_keys
            .iter()
            .filter(|key| !file.keys.contains(key))
            .map(|key| key.to_owned())
            .collect();

        if !missing_keys.is_empty() {
            let warning = CompareWarning {
                path: file.path,
                missing_keys,
            };

            warnings.push(warning)
        }
    }

    output.print_warnings(&warnings);
    Ok(warnings)
}

fn get_lines(args: &clap::ArgMatches, current_dir: &Path) -> BTreeMap<FileEntry, Vec<String>> {
    let file_paths: Vec<PathBuf> = get_needed_file_paths(args);

    file_paths
        .iter()
        .map(|path| fs_utils::get_relative_path(path, current_dir).and_then(FileEntry::from))
        .flatten()
        .collect::<BTreeMap<_, _>>()
}

/// Getting a list of all files for checking/fixing without custom exclusion files
fn get_needed_file_paths(args: &clap::ArgMatches) -> Vec<PathBuf> {
    let mut file_paths: Vec<PathBuf> = Vec::new();
    let mut excluded_paths: Vec<PathBuf> = Vec::new();

    let is_recursive = args.is_present("recursive");

    if let Some(excluded) = args.values_of("exclude") {
        excluded_paths = excluded
            .filter_map(|f| fs_utils::canonicalize(f).ok())
            .collect();
    }

    if let Some(inputs) = args.values_of("input") {
        let input_paths = inputs
            .filter_map(|s| fs_utils::canonicalize(s).ok())
            .collect();

        file_paths.extend(get_file_paths(input_paths, &excluded_paths, is_recursive));
    }

    file_paths
}

fn get_file_paths(
    dir_entries: Vec<PathBuf>,
    excludes: &[PathBuf],
    is_recursive: bool,
) -> Vec<PathBuf> {
    let nested_paths: Vec<PathBuf> = dir_entries
        .iter()
        .filter(|entry| entry.is_dir())
        .filter(|entry| !excludes.contains(entry))
        .filter_map(|dir| dir.read_dir().ok())
        .map(|read_dir| {
            read_dir
                .filter_map(|e| e.ok())
                .map(|e| e.path())
                .filter(|path| {
                    FileEntry::is_env_file(path)
                        || (is_recursive && path.is_dir() && path.read_link().is_err())
                })
                .collect()
        })
        .flat_map(|dir_entries| get_file_paths(dir_entries, excludes, is_recursive))
        .collect();

    let mut file_paths: Vec<PathBuf> = dir_entries
        .into_iter()
        .filter(|entry| entry.is_file())
        .filter(|entry| !excludes.contains(entry))
        .collect();

    file_paths.extend(nested_paths);
    file_paths.sort();
    file_paths.dedup();
    file_paths
}

fn get_line_entries(fe: &FileEntry, lines: Vec<String>) -> Vec<LineEntry> {
    let fe = Rc::new(fe.clone());
    lines
        .into_iter()
        .enumerate()
        .map(|(index, line)| LineEntry::new(index + 1, fe.clone(), line))
        .collect()
}