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
#![warn(unused_extern_crates)]
#![allow(clippy::missing_errors_doc)]

use std::fs;

use api::Solution;
use color_eyre::{eyre::Context, Result};
use jwalk::{Parallelism, WalkDir};

pub mod api;
mod ast;
mod lex;
pub mod msbuild;
mod parser;

#[macro_use]
extern crate lalrpop_util;

#[cfg(test)] // <-- not needed in integration tests
extern crate rstest;

lalrpop_mod!(
    #[allow(clippy::all)]
    #[allow(unused)]
    solp
);

/// Consume provides parsed solution consumer
pub trait Consume {
    /// Called in case of success parsing
    fn ok(&mut self, solution: &Solution);
    /// Called on error
    fn err(&self, path: &str);
}

/// `parse_file` parses single solution file specified by path..
///
/// # Errors
///
/// This function will return an error if file content cannot be read into memory
/// or solution file has invalid syntax.
pub fn parse_file(path: &str, consumer: &mut dyn Consume) -> Result<()> {
    let contents = fs::read_to_string(path).wrap_err_with(|| {
        consumer.err(path);
        format!("Failed to read content from path: {path}")
    })?;
    let mut solution = parse_str(&contents).wrap_err_with(|| {
        consumer.err(path);
        format!("Failed to parse solution from path: {path}")
    })?;

    solution.path = path;
    consumer.ok(&solution);
    Ok(())
}

/// `parse_str` parses solution content from `&str` and returns [`Solution`] in case of success
///
/// # Errors
///
/// This function will return an error if solution file has invalid syntax or corrupted.
pub fn parse_str(contents: &str) -> Result<Solution> {
    let parsed = parser::parse_str(contents)?;
    Ok(Solution::from(&parsed))
}

/// `parse_dir` parses only directory specified by path.
/// it finds all files with extension specified and parses them.
/// returns the number of scanned solutions
///
/// ## Remarks
/// Any errors occured during parsing of found files will be ignored (so parsing won't stopped)
/// but error paths will be added into error files list (using err function of [`Consume`] trait)
pub fn parse_dir(path: &str, extension: &str, consumer: &mut dyn Consume) -> usize {
    let iter = create_dir_iterator(path).max_depth(1);
    parse_dir_or_tree(iter, extension, consumer)
}

/// `parse_dir_tree` parses directory specified by path. recursively
/// it finds all files with extension specified and parses them.
/// returns the number of scanned solutions
///
/// ## Remarks
/// Any errors occured during parsing of found files will be ignored (so parsing won't stopped)
/// but error paths will be added into error files list (using err function of [`Consume`] trait)
pub fn parse_dir_tree(path: &str, extension: &str, consumer: &mut dyn Consume) -> usize {
    let parallelism = Parallelism::RayonNewPool(num_cpus::get_physical());
    let iter = create_dir_iterator(path).parallelism(parallelism);
    parse_dir_or_tree(iter, extension, consumer)
}

fn create_dir_iterator(path: &str) -> WalkDir {
    let root = decorate_path(path);
    WalkDir::new(root).skip_hidden(false).follow_links(false)
}

fn parse_dir_or_tree(iter: WalkDir, extension: &str, consumer: &mut dyn Consume) -> usize {
    let ext = extension.trim_start_matches('.');

    iter.into_iter()
        .filter_map(std::result::Result::ok)
        .filter(|f| f.file_type().is_file())
        .map(|f| f.path())
        .filter(|p| p.extension().map(|s| s == ext).unwrap_or_default())
        .map(|f| f.to_str().unwrap_or("").to_string())
        .filter_map(|fp| parse_file(&fp, consumer).ok())
        .count()
}

/// On Windows trailing back slash (\) to be added if volume and colon passed (like c:).
/// It needed paths look to be more pleasant
#[cfg(target_os = "windows")]
fn decorate_path(path: &str) -> String {
    if path.len() == 2 && path.ends_with(':') {
        format!("{path}\\")
    } else {
        path.to_owned()
    }
}

/// On Unix just passthrough as is
#[cfg(not(target_os = "windows"))]
fn decorate_path(path: &str) -> String {
    path.to_owned()
}

#[cfg(test)]
mod tests {
    use super::*;
    use rstest::rstest;

    #[cfg(not(target_os = "windows"))]
    #[rstest]
    #[case("", "")]
    #[case("/", "/")]
    #[case("/home", "/home")]
    #[case("d:", "d:")]
    #[trace]
    fn decorate_path_tests(#[case] raw_path: &str, #[case] expected: &str) {
        // Arrange

        // Act
        let actual = decorate_path(raw_path);

        // Assert
        assert_eq!(actual, expected);
    }

    #[cfg(target_os = "windows")]
    #[rstest]
    #[case("", "")]
    #[case("/", "/")]
    #[case("d:", "d:\\")]
    #[case("dd:", "dd:")]
    #[trace]
    fn decorate_path_tests(#[case] raw_path: &str, #[case] expected: &str) {
        // Arrange

        // Act
        let actual = decorate_path(raw_path);

        // Assert
        assert_eq!(actual, expected);
    }
}