solp/lib.rs
1/*!
2A library for parsing Microsoft Visual Studio solution file
3
4
5## Example: parsing solution from [&str]
6
7```
8use solp::parse_str;
9
10const SOLUTION: &str = r#"
11Microsoft Visual Studio Solution File, Format Version 12.00
12Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "bench", "bench\bench.csproj", "{A61CD222-0F3B-47B6-9F7F-25D658368EEC}"
13EndProject
14Global
15 GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 Debug|Any CPU = Debug|Any CPU
17 Release|Any CPU = Release|Any CPU
18 EndGlobalSection
19 GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Release|Any CPU.Build.0 = Release|Any CPU
24 EndGlobalSection
25EndGlobal
26"#;
27
28let result = parse_str(SOLUTION);
29assert!(result.is_ok());
30let solution = result.unwrap();
31assert_eq!(solution.projects.len(), 1);
32assert_eq!(solution.configurations.len(), 2);
33assert_eq!(solution.format, "12.00");
34
35```
36*/
37
38#![warn(unused_extern_crates)]
39#![allow(clippy::missing_errors_doc)]
40use std::fs;
41
42use api::Solution;
43use jwalk::{Parallelism, WalkDir};
44use miette::{Context, IntoDiagnostic};
45
46pub mod api;
47mod ast;
48mod lex;
49pub mod msbuild;
50mod parser;
51
52#[macro_use]
53extern crate lalrpop_util;
54
55#[cfg(test)] // <-- not needed in integration tests
56extern crate rstest;
57
58lalrpop_mod!(
59 #[allow(clippy::all)]
60 #[allow(unused)]
61 #[allow(clippy::no_effect_underscore_binding)]
62 #[allow(clippy::trivially_copy_pass_by_ref)]
63 #[allow(clippy::cloned_instead_of_copied)]
64 #[allow(clippy::cast_sign_loss)]
65 #[allow(clippy::too_many_lines)]
66 #[allow(clippy::match_same_arms)]
67 #[allow(clippy::uninlined_format_args)]
68 #[allow(clippy::unused_self)]
69 #[allow(clippy::needless_raw_string_hashes)]
70 solp
71);
72
73/// Default Visual Studio solution file extension
74pub const DEFAULT_SOLUTION_EXT: &str = "sln";
75
76/// Consume provides parsed [`Solution`] consumer
77pub trait Consume {
78 /// Called in case of success parsing
79 fn ok(&mut self, solution: &Solution);
80 /// Called on error
81 fn err(&self, path: &str);
82}
83
84/// Builder for walking a directory structure.
85pub struct SolpWalker<'a, C: Consume> {
86 /// [`Consume`] trait instance that will be applied to each file scanned
87 pub consumer: C,
88 extension: &'a str,
89 show_errors: bool,
90 recursively: bool,
91}
92
93/// Parses a solution file at the specified path and notifies the consumer of the result.
94///
95/// This function reads the content of the file at the given path and attempts to parse it
96/// as a Microsoft Visual Studio solution file. If the file is successfully read and parsed,
97/// the consumer's `ok` method is called with the parsed `Solution`. If any errors occur during
98/// reading or parsing, the consumer's `err` method is called with the path of the file, and an
99/// error is returned.
100///
101/// # Parameters
102///
103/// - `path`: A string slice that holds the path to the solution file.
104/// - `consumer`: A mutable reference to an object that implements the `Consume` trait. This consumer
105/// will be notified of the result of the parse operation.
106///
107/// # Returns
108///
109/// A `Result` which is `Ok(())` if the file was successfully read and parsed, or an error if any
110/// issues occurred during reading or parsing.
111///
112/// # Errors
113///
114/// This function will return an error if the file cannot be read or if the content cannot be parsed
115/// as a valid solution file. In both cases, the consumer's `err` method will be called with the path
116/// of the file.
117///
118/// # Example
119///
120/// ```rust
121/// use solp::parse_file;
122/// use solp::api::Solution;
123/// use solp::Consume;
124///
125/// struct Consumer;
126///
127/// impl Consume for Consumer {
128/// fn ok(&mut self, solution: &Solution) {
129/// // ...
130/// }
131///
132/// fn err(&self, path: &str) {
133/// // ...
134/// }
135/// }
136///
137/// let path = "path/to/solution.sln";
138/// let mut consumer = Consumer{};
139/// match parse_file(path, &mut consumer) {
140/// Ok(()) => println!("Successfully parsed the solution file."),
141/// Err(e) => eprintln!("Failed to parse the solution file: {:?}", e),
142/// }
143/// ```
144pub fn parse_file(path: &str, consumer: &mut dyn Consume) -> miette::Result<()> {
145 let contents = fs::read_to_string(path)
146 .into_diagnostic()
147 .wrap_err_with(|| {
148 consumer.err(path);
149 format!("Failed to read content from path: {path}")
150 })?;
151 let mut solution = parse_str(&contents).wrap_err_with(|| {
152 consumer.err(path);
153 format!("Failed to parse solution from path: {path}")
154 })?;
155
156 solution.path = path;
157 consumer.ok(&solution);
158 Ok(())
159}
160
161/// Parses a solution file content from a string slice and returns a [`Solution`] object.
162///
163/// This function takes the content of a solution file as a string slice, attempts to parse it,
164/// and returns a `Solution` object representing the parsed content. If parsing fails, an error
165/// is returned.
166///
167/// # Parameters
168///
169/// - `contents`: A string slice that holds the content of the solution file to be parsed.
170///
171/// # Returns
172///
173/// A `Result` containing a [`Solution`] object if parsing is successful, or an error if parsing fails.
174///
175/// # Errors
176///
177/// This function will return an error if the content cannot be parsed as a valid solution file.
178///
179/// # Example
180///
181/// ```rust
182/// use solp::parse_str;
183///
184/// let solution_content = r#"
185/// Microsoft Visual Studio Solution File, Format Version 12.00
186/// # Visual Studio 16
187/// VisualStudioVersion = 16.0.28701.123
188/// MinimumVisualStudioVersion = 10.0.40219.1
189/// Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyProject", "MyProject.csproj", "{A61CD222-0F3B-47B6-9F7F-25D658368EEC}"
190/// EndProject
191/// Global
192/// GlobalSection(SolutionConfigurationPlatforms) = preSolution
193/// Debug|Any CPU = Debug|Any CPU
194/// Release|Any CPU = Release|Any CPU
195/// EndGlobalSection
196/// GlobalSection(ProjectConfigurationPlatforms) = postSolution
197/// {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
198/// {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
199/// {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
200/// {A61CD222-0F3B-47B6-9F7F-25D658368EEC}.Release|Any CPU.Build.0 = Release|Any CPU
201/// EndGlobalSection
202/// EndGlobal
203/// "#;
204///
205/// parse_str(solution_content);
206/// // This will return a Result containing a Solution object if parsing is successful.
207/// ```
208///
209/// # Remarks
210///
211/// This function uses the `parser::parse_str` function to perform the actual parsing and then
212/// constructs a [`Solution`] object from the parsed data.
213pub fn parse_str(contents: &str) -> miette::Result<Solution> {
214 let parsed = parser::parse_str(contents)?;
215 Ok(Solution::from(&parsed))
216}
217
218impl<'a, C: Consume> SolpWalker<'a, C> {
219 /// Create a builder for a directory structure parsing.
220 pub fn new(consumer: C) -> Self {
221 Self {
222 consumer,
223 extension: DEFAULT_SOLUTION_EXT,
224 show_errors: false,
225 recursively: false,
226 }
227 }
228
229 /// Setting Visual Studio solution file extension. sln by default.
230 #[must_use]
231 pub fn with_extension(mut self, extension: &'a str) -> Self {
232 self.extension = extension;
233 self
234 }
235
236 /// Scan recursively. Disabled by default.
237 #[must_use]
238 pub fn recursively(mut self, recursively: bool) -> Self {
239 self.recursively = recursively;
240 self
241 }
242
243 /// Whether to show parsing errors during directory scanning. Disabled by default.
244 #[must_use]
245 pub fn show_errors(mut self, show_errors: bool) -> Self {
246 self.show_errors = show_errors;
247 self
248 }
249
250 /// `walk_and_parse` parses directory structure specified by path.
251 /// it finds all files with extension specified and parses them.
252 /// returns the number of scanned solutions
253 ///
254 /// ## Remarks
255 /// Any errors occurred during parsing of found files will be ignored (so parsing won't stopped)
256 /// but error paths will be added into error files list (using err function of [`Consume`] trait)
257 pub fn walk_and_parse(&mut self, path: &str) -> usize {
258 let iter = if self.recursively {
259 let parallelism = Parallelism::RayonNewPool(num_cpus::get_physical());
260 create_dir_iterator(path).parallelism(parallelism)
261 } else {
262 create_dir_iterator(path).max_depth(1)
263 };
264 let ext = self.extension.trim_start_matches('.');
265
266 iter.into_iter()
267 .filter_map(std::result::Result::ok)
268 .filter(|f| f.file_type().is_file())
269 .map(|f| f.path())
270 .filter(|p| p.extension().is_some_and(|s| s == ext))
271 .filter_map(|fp| {
272 let p = fp.to_str()?;
273 if let Err(e) = parse_file(p, &mut self.consumer) {
274 if self.show_errors {
275 println!("{e:?}");
276 }
277 None
278 } else {
279 Some(())
280 }
281 })
282 .count()
283 }
284}
285
286fn create_dir_iterator(path: &str) -> WalkDir {
287 let root = decorate_path(path);
288 WalkDir::new(root).skip_hidden(false).follow_links(false)
289}
290
291/// On Windows trailing backslash (\) to be added if volume and colon passed (like c:).
292/// It needed paths look to be more pleasant
293#[cfg(target_os = "windows")]
294fn decorate_path(path: &str) -> String {
295 if path.len() == 2 && path.ends_with(':') {
296 format!("{path}\\")
297 } else {
298 path.to_owned()
299 }
300}
301
302/// On Unix just pass-through as is
303#[cfg(not(target_os = "windows"))]
304fn decorate_path(path: &str) -> String {
305 path.to_owned()
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use rstest::rstest;
312
313 #[cfg(not(target_os = "windows"))]
314 #[rstest]
315 #[case("", "")]
316 #[case("/", "/")]
317 #[case("/home", "/home")]
318 #[case("d:", "d:")]
319 #[trace]
320 fn decorate_path_tests(#[case] raw_path: &str, #[case] expected: &str) {
321 // Arrange
322
323 // Act
324 let actual = decorate_path(raw_path);
325
326 // Assert
327 assert_eq!(actual, expected);
328 }
329
330 #[cfg(target_os = "windows")]
331 #[rstest]
332 #[case("", "")]
333 #[case("/", "/")]
334 #[case("d:", "d:\\")]
335 #[case("dd:", "dd:")]
336 #[trace]
337 fn decorate_path_tests(#[case] raw_path: &str, #[case] expected: &str) {
338 // Arrange
339
340 // Act
341 let actual = decorate_path(raw_path);
342
343 // Assert
344 assert_eq!(actual, expected);
345 }
346}