#![warn(unused_extern_crates)]
#![allow(clippy::missing_errors_doc)]
use std::fs;
use api::Solution;
use jwalk::{Parallelism, WalkDir};
use miette::{Context, IntoDiagnostic};
pub mod api;
mod ast;
mod lex;
pub mod msbuild;
mod parser;
#[macro_use]
extern crate lalrpop_util;
lalrpop_mod!(
#[allow(clippy::all)]
#[allow(unused)]
#[allow(clippy::no_effect_underscore_binding)]
#[allow(clippy::trivially_copy_pass_by_ref)]
#[allow(clippy::cloned_instead_of_copied)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::too_many_lines)]
#[allow(clippy::match_same_arms)]
#[allow(clippy::uninlined_format_args)]
#[allow(clippy::unused_self)]
#[allow(clippy::needless_raw_string_hashes)]
#[allow(clippy::elidable_lifetime_names)]
solp
);
pub const DEFAULT_SOLUTION_EXT: &str = "sln";
pub trait Consume {
fn ok(&mut self, solution: &Solution);
fn err(&self, path: &str);
}
pub struct SolpWalker<'a, C: Consume> {
pub consumer: C,
extension: &'a str,
show_errors: bool,
recursively: bool,
}
pub fn parse_file(path: &str, consumer: &mut dyn Consume) -> miette::Result<()> {
let contents = fs::read_to_string(path)
.into_diagnostic()
.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(())
}
pub fn parse_str(contents: &'_ str) -> miette::Result<Solution<'_>> {
let parsed = parser::parse_str(contents)?;
Ok(Solution::from(&parsed))
}
impl<'a, C: Consume> SolpWalker<'a, C> {
pub fn new(consumer: C) -> Self {
Self {
consumer,
extension: DEFAULT_SOLUTION_EXT,
show_errors: false,
recursively: false,
}
}
#[must_use]
pub fn with_extension(mut self, extension: &'a str) -> Self {
self.extension = extension;
self
}
#[must_use]
pub fn recursively(mut self, recursively: bool) -> Self {
self.recursively = recursively;
self
}
#[must_use]
pub fn show_errors(mut self, show_errors: bool) -> Self {
self.show_errors = show_errors;
self
}
pub fn walk_and_parse(&mut self, path: &str) -> usize {
let iter = if self.recursively {
let parallelism = Parallelism::RayonNewPool(num_cpus::get_physical());
create_dir_iterator(path).parallelism(parallelism)
} else {
create_dir_iterator(path).max_depth(1)
};
let ext = self.extension.trim_start_matches('.');
iter.into_iter()
.filter_map(Result::ok)
.filter(|f| f.file_type().is_file())
.map(|f| f.path())
.filter(|p| p.extension().is_some_and(|s| s == ext))
.filter_map(|fp| {
let p = fp.to_str()?;
if let Err(e) = parse_file(p, &mut self.consumer) {
if self.show_errors {
println!("{e:?}");
}
None
} else {
Some(())
}
})
.count()
}
}
fn create_dir_iterator(path: &str) -> WalkDir {
let root = decorate_path(path);
WalkDir::new(root).skip_hidden(false).follow_links(false)
}
#[cfg(target_os = "windows")]
fn decorate_path(path: &str) -> String {
if path.len() == 2 && path.ends_with(':') {
format!("{path}\\")
} else {
path.to_owned()
}
}
#[cfg(not(target_os = "windows"))]
fn decorate_path(path: &str) -> String {
path.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[cfg(not(target_os = "windows"))]
#[test_case("", "" ; "empty")]
#[test_case("/", "/")]
#[test_case("/home", "/home")]
#[test_case("d:", "d:")]
fn decorate_path_tests(raw_path: &str, expected: &str) {
let actual = decorate_path(raw_path);
assert_eq!(actual, expected);
}
#[cfg(target_os = "windows")]
#[test_case("", "" ; "empty")]
#[test_case("/", "/")]
#[test_case("d:", "d:\\")]
#[test_case("dd:", "dd:")]
fn decorate_path_tests(raw_path: &str, expected: &str) {
let actual = decorate_path(raw_path);
assert_eq!(actual, expected);
}
}