solp/
ast.rs

1use crate::msbuild;
2use nom::{
3    IResult, Parser,
4    branch::alt,
5    bytes::complete::{is_not, tag, take_until},
6    character::complete::{self, char},
7    combinator::{self, recognize},
8    error::{Error, ParseError},
9    sequence::{self, pair},
10};
11
12const ACTIVE_CFG_TAG: &str = ".ActiveCfg";
13const BUILD_TAG: &str = ".Build.0";
14const DEPLOY_TAG: &str = ".Deploy.0";
15
16/// Represents AST node type
17#[derive(Debug)]
18pub enum Node<'a> {
19    Comment(&'a str),
20    Version(&'a str, &'a str),
21    FirstLine(&'a str),
22    Global(Vec<Node<'a>>),
23    Project(Box<Node<'a>>, Vec<Node<'a>>),
24    ProjectBegin(&'a str, &'a str, &'a str, &'a str),
25    Section(Box<Node<'a>>, Vec<Node<'a>>),
26    SectionBegin(Vec<&'a str>, &'a str),
27    SectionContent(&'a str, &'a str),
28    Solution(Box<Node<'a>>, Vec<Node<'a>>),
29}
30
31/// Visual Studio solution file (.sln) model
32#[derive(Debug, Clone, Default)]
33pub struct Sol<'a> {
34    /// Path to solution file. Maybe empty string
35    /// because solution can be parsed using memory data.
36    pub path: &'a str,
37    pub format: &'a str,
38    pub product: &'a str,
39    pub projects: Vec<Prj<'a>>,
40    pub versions: Vec<Ver<'a>>,
41    pub solution_configs: Vec<Conf<'a>>,
42    pub project_configs: Vec<PrjConfAggregate<'a>>,
43}
44
45/// Solution version descriptor
46#[derive(Debug, Copy, Clone)]
47pub struct Ver<'a> {
48    pub name: &'a str,
49    pub ver: &'a str,
50}
51
52/// Project configurations aggregator
53#[derive(Debug, Clone)]
54pub struct PrjConfAggregate<'a> {
55    pub project_id: &'a str,
56    pub configs: Vec<PrjConf<'a>>,
57}
58
59/// Configuration and platform pair
60#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
61pub struct Conf<'a> {
62    pub config: &'a str,
63    pub platform: &'a str,
64}
65
66/// Project model
67#[derive(Debug, Clone, Default)]
68pub struct Prj<'a> {
69    pub type_id: &'a str,
70    pub type_descr: &'a str,
71    pub id: &'a str,
72    pub name: &'a str,
73    pub path_or_uri: &'a str,
74    pub items: Vec<&'a str>,
75    pub depends_from: Vec<&'a str>,
76}
77
78impl<'a> Prj<'a> {
79    #[must_use]
80    pub fn new(id: &'a str, type_id: &'a str) -> Self {
81        let type_descr = msbuild::describe_project(type_id);
82
83        Self {
84            type_id,
85            type_descr,
86            id,
87            ..Default::default()
88        }
89    }
90
91    #[must_use]
92    pub fn from_begin(head: &Node<'a>) -> Option<Self> {
93        if let Node::ProjectBegin(project_type, name, path_or_uri, id) = head {
94            let prj = Prj::from(project_type, name, path_or_uri, id);
95            Some(prj)
96        } else {
97            None
98        }
99    }
100
101    #[must_use]
102    pub fn from(project_type: &'a str, name: &'a str, path_or_uri: &'a str, id: &'a str) -> Self {
103        let mut prj = Prj::new(id, project_type);
104        prj.name = name;
105        prj.path_or_uri = path_or_uri;
106
107        prj
108    }
109}
110
111impl<'a> Ver<'a> {
112    #[must_use]
113    pub fn new(name: &'a str, ver: &'a str) -> Self {
114        Self { name, ver }
115    }
116
117    #[must_use]
118    pub fn from(name: &'a str, val: &'a str) -> Self {
119        Ver::new(name, val)
120    }
121}
122
123impl<'a> From<&'a str> for Conf<'a> {
124    fn from(s: &'a str) -> Self {
125        pipe_terminated::<Error<&str>>(s)
126            .map(|(platform, config)| Self { config, platform })
127            .unwrap_or_default()
128    }
129}
130
131impl<'a> Conf<'a> {
132    #[must_use]
133    pub fn new(configuration: &'a str, platform: &'a str) -> Self {
134        Self {
135            config: configuration,
136            platform,
137        }
138    }
139
140    #[must_use]
141    pub fn from_node(node: &Node<'a>) -> Option<Self> {
142        if let Node::SectionContent(left, _) = node {
143            let conf = Conf::from(*left);
144            Some(conf)
145        } else {
146            None
147        }
148    }
149}
150
151#[derive(Default, PartialEq, Debug, Clone)]
152pub struct PrjConf<'a> {
153    pub id: &'a str,
154    pub solution_config: &'a str,
155    pub project_config: &'a str,
156    pub platform: &'a str,
157    pub tag: ProjectConfigTag,
158}
159
160#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
161pub enum ProjectConfigTag {
162    #[default]
163    ActiveCfg,
164    Build,
165    Deploy,
166}
167
168impl<'a> PrjConfAggregate<'a> {
169    #[must_use]
170    pub fn from_id_and_configs(project_id: &'a str, configs: Vec<PrjConf<'a>>) -> Self {
171        Self {
172            project_id,
173            configs,
174        }
175    }
176
177    #[must_use]
178    pub fn handle_project_config_platform(node: &Node<'a>) -> Option<Self> {
179        if let Node::SectionContent(left, right) = node {
180            PrjConfAggregate::from_project_configuration_platform(left, right)
181        } else {
182            None
183        }
184    }
185
186    #[must_use]
187    pub fn handle_project_config(node: &Node<'a>) -> Option<Self> {
188        if let Node::SectionContent(left, right) = node {
189            PrjConfAggregate::from_project_configuration(left, right)
190        } else {
191            None
192        }
193    }
194
195    fn from_project_configuration_platform(k: &'a str, v: &'a str) -> Option<Self> {
196        let r = PrjConfAggregate::parse_project_configuration_platform::<Error<&str>>(k, v);
197        Self::new(r)
198    }
199
200    fn from_project_configuration(k: &'a str, v: &'a str) -> Option<Self> {
201        let r = PrjConfAggregate::parse_project_configuration::<Error<&str>>(k, v);
202        Self::new(r)
203    }
204
205    fn new(r: IResult<&'a str, PrjConf<'a>, Error<&'a str>>) -> Option<Self> {
206        r.ok().map(|(_, pc)| Self {
207            project_id: pc.id,
208            configs: vec![pc],
209        })
210    }
211
212    // Configuration, platform parsing made by using nom crate that implement parser combinators
213    // method. See more about idea https://en.wikipedia.org/wiki/Parser_combinator
214
215    fn parse_project_configuration_platform<'b, E>(
216        key: &'b str,
217        value: &'b str,
218    ) -> IResult<&'b str, PrjConf<'b>, E>
219    where
220        E: ParseError<&'b str> + std::fmt::Debug,
221    {
222        let parser =
223            sequence::separated_pair(guid, char('.'), pair(pipe_terminated, tag_terminated));
224
225        let project_conf = Conf::from(value);
226
227        combinator::map(parser, |(project_id, (solution_config, platform))| {
228            PrjConf {
229                id: project_id,
230                solution_config,
231                project_config: project_conf.config,
232                platform,
233                tag: define_tag(key),
234            }
235        })
236        .parse(key)
237    }
238
239    fn parse_project_configuration<'b, E>(
240        key: &'b str,
241        value: &'b str,
242    ) -> IResult<&'b str, PrjConf<'b>, E>
243    where
244        E: ParseError<&'b str> + std::fmt::Debug,
245    {
246        let parser = sequence::separated_pair(guid, char('.'), tag_terminated);
247
248        let project_conf = Conf::from(value);
249
250        combinator::map(parser, |(project_id, solution_config)| PrjConf {
251            id: project_id,
252            solution_config,
253            project_config: project_conf.config,
254            platform: project_conf.platform,
255            tag: define_tag(key),
256        })
257        .parse(key)
258    }
259}
260
261fn define_tag(key: &str) -> ProjectConfigTag {
262    if key.ends_with(BUILD_TAG) {
263        ProjectConfigTag::Build
264    } else if key.ends_with(DEPLOY_TAG) {
265        ProjectConfigTag::Deploy
266    } else {
267        ProjectConfigTag::ActiveCfg
268    }
269}
270
271fn guid<'a, E>(input: &'a str) -> IResult<&'a str, &'a str, E>
272where
273    E: ParseError<&'a str> + std::fmt::Debug,
274{
275    recognize(sequence::delimited(
276        complete::char('{'),
277        is_not("{}"),
278        complete::char('}'),
279    ))
280    .parse(input)
281}
282
283fn tag_terminated<'a, E>(input: &'a str) -> IResult<&'a str, &'a str, E>
284where
285    E: ParseError<&'a str> + std::fmt::Debug,
286{
287    sequence::terminated(
288        alt((
289            take_until(ACTIVE_CFG_TAG),
290            take_until(BUILD_TAG),
291            take_until(DEPLOY_TAG),
292        )),
293        alt((tag(ACTIVE_CFG_TAG), tag(BUILD_TAG), tag(DEPLOY_TAG))),
294    )
295    .parse(input)
296}
297
298fn pipe_terminated<'a, E>(input: &'a str) -> IResult<&'a str, &'a str, E>
299where
300    E: ParseError<&'a str> + std::fmt::Debug,
301{
302    sequence::terminated(is_not("|"), char('|')).parse(input)
303}
304
305impl Node<'_> {
306    #[must_use]
307    pub fn is_section(&self, name: &str) -> bool {
308        if let Node::SectionBegin(names, _) = self {
309            names.iter().any(|n| *n == name)
310        } else {
311            false
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use rstest::rstest;
320
321    #[rstest]
322    #[case("Release|Any CPU", Conf { config: "Release", platform: "Any CPU" })]
323    #[case("", Conf { config: "", platform: "" })]
324    #[case("Release Any CPU", Conf { config: "", platform: "" })]
325    #[case("Release|Any CPU|test", Conf { config: "Release", platform: "Any CPU|test" })]
326    #[trace]
327    fn from_configuration_tests(#[case] i: &str, #[case] expected: Conf) {
328        // Arrange
329
330        // Act
331        let c = Conf::from(i);
332
333        // Assert
334        assert_eq!(c, expected);
335    }
336
337    #[test]
338    fn from_project_configurations_correct() {
339        // Arrange
340        let k = "{27060CA7-FB29-42BC-BA66-7FC80D498354}.Debug|Any CPU.ActiveCfg";
341        let v = "Debug|x86";
342
343        // Act
344        let c = PrjConfAggregate::from_project_configuration_platform(k, v);
345
346        // Assert
347        assert!(c.is_some());
348        let c = c.unwrap();
349        assert_eq!(c.project_id, "{27060CA7-FB29-42BC-BA66-7FC80D498354}");
350        assert_eq!(c.configs.len(), 1);
351        assert_eq!(c.configs[0].solution_config, "Debug");
352        assert_eq!(c.configs[0].project_config, "Debug");
353        assert_eq!(c.configs[0].platform, "Any CPU");
354    }
355
356    #[test]
357    fn from_project_configurations_config_with_dot() {
358        // Arrange
359        let k = "{27060CA7-FB29-42BC-BA66-7FC80D498354}.Debug .NET 4.0|Any CPU.ActiveCfg";
360        let v = "Debug|x86";
361
362        // Act
363        let c = PrjConfAggregate::from_project_configuration_platform(k, v);
364
365        // Assert
366        assert!(c.is_some());
367        let c = c.unwrap();
368        assert_eq!(c.project_id, "{27060CA7-FB29-42BC-BA66-7FC80D498354}");
369        assert_eq!(c.configs.len(), 1);
370        assert_eq!(c.configs[0].solution_config, "Debug .NET 4.0");
371        assert_eq!(c.configs[0].project_config, "Debug");
372        assert_eq!(c.configs[0].platform, "Any CPU");
373    }
374
375    #[test]
376    fn from_project_configurations_platform_with_dot_active() {
377        // Arrange
378        let k = "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}.Release|.NET.ActiveCfg";
379        let v = "Release|x86";
380
381        // Act
382        let c = PrjConfAggregate::from_project_configuration_platform(k, v);
383
384        // Assert
385        assert!(c.is_some());
386        let c = c.unwrap();
387        assert_eq!(c.project_id, "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}");
388        assert_eq!(c.configs.len(), 1);
389        assert_eq!(c.configs[0].solution_config, "Release");
390        assert_eq!(c.configs[0].project_config, "Release");
391        assert_eq!(c.configs[0].platform, ".NET");
392    }
393
394    #[test]
395    fn from_project_configurations_without_platform() {
396        // Arrange
397        let k = "{5228E9CE-A216-422F-A5E6-58E95E2DD71D}.DLL Debug.ActiveCfg";
398        let v = "Debug|x86";
399
400        // Act
401        let c = PrjConfAggregate::from_project_configuration_platform(k, v);
402
403        // Assert
404        assert!(c.is_none());
405    }
406
407    #[test]
408    fn guid_test() {
409        // Arrange
410        let s = "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}.Release|.NET.Build.0";
411
412        // Act
413        let result = guid::<Error<&str>>(s);
414
415        // Assert
416        assert_eq!(
417            result,
418            Ok((
419                ".Release|.NET.Build.0",
420                "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}",
421            ))
422        );
423    }
424
425    #[rstest]
426    #[case(".NET.Build.0", ".NET")]
427    #[case(".NET.ActiveCfg", ".NET")]
428    #[trace]
429    fn tag_terminated_tests(#[case] i: &str, #[case] expected: &str) {
430        // Arrange
431
432        // Act
433        let result = tag_terminated::<Error<&str>>(i);
434
435        // Assert
436        assert_eq!(result, Ok(("", expected)));
437    }
438
439    #[rstest]
440    #[case("{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}.Release|.NET.Build.0", "Release|.NET", PrjConf { id: "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}", solution_config: "Release", project_config: "Release", platform: ".NET", tag: ProjectConfigTag::Build })]
441    #[case("{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}.SolutionRelease|.NET.Build.0", "ProjectRelease|.NET", PrjConf { id: "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}", solution_config: "SolutionRelease", project_config: "ProjectRelease", platform: ".NET", tag: ProjectConfigTag::Build })]
442    #[case("{60BB14A5-0871-4656-BC38-4F0958230F9A}.Debug|ARM.Deploy.0", "Debug|ARM", PrjConf { id: "{60BB14A5-0871-4656-BC38-4F0958230F9A}", solution_config: "Debug", project_config: "Debug", platform: "ARM", tag: ProjectConfigTag::Deploy })]
443    #[case("{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}.Release|.NET.ActiveCfg", "Release|.NET", PrjConf { id: "{7C2EF610-BCA0-4D1F-898A-DE9908E4970C}", solution_config: "Release", project_config: "Release", platform: ".NET", tag: ProjectConfigTag::ActiveCfg })]
444    #[trace]
445    fn project_configs_parse_project_configuration_platform_tests(
446        #[case] k: &str,
447        #[case] v: &str,
448        #[case] expected: PrjConf,
449    ) {
450        // Arrange
451
452        // Act
453        let result = PrjConfAggregate::parse_project_configuration_platform::<Error<&str>>(k, v);
454
455        // Assert
456        assert_eq!(result, Ok(("", expected)));
457    }
458
459    #[rstest]
460    #[case("{5228E9CE-A216-422F-A5E6-58E95E2DD71D}.DLL Debug.ActiveCfg", "Debug|x64", PrjConf { id: "{5228E9CE-A216-422F-A5E6-58E95E2DD71D}", solution_config: "DLL Debug", project_config: "Debug", platform: "x64", tag: ProjectConfigTag::ActiveCfg })]
461    #[trace]
462    fn project_configs_parse_project_configuration_tests(
463        #[case] k: &str,
464        #[case] v: &str,
465        #[case] expected: PrjConf,
466    ) {
467        // Arrange
468
469        // Act
470        let result = PrjConfAggregate::parse_project_configuration::<Error<&str>>(k, v);
471
472        // Assert
473        assert_eq!(result, Ok(("", expected)));
474    }
475}