From bd66e71539641db775f7d3df8f8219034eee2b3d Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 18 Jul 2017 11:33:40 -0700 Subject: [PATCH 1/3] Add SolutionFile class and initial test --- .../SolutionParsing/SolutionFile.cs | 21 +++++++++++++++++++ .../SolutionParsingTests.cs | 15 +++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs create mode 100644 tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs diff --git a/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs b/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs new file mode 100644 index 0000000000..1bca7fec01 --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs @@ -0,0 +1,21 @@ +using System; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal sealed class SolutionFile + { + private SolutionFile() + { + } + + public static SolutionFile Parse(string text) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + return new SolutionFile(); + } + } +} diff --git a/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs b/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs new file mode 100644 index 0000000000..93960208ef --- /dev/null +++ b/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs @@ -0,0 +1,15 @@ +using System; +using OmniSharp.MSBuild.SolutionParsing; +using Xunit; + +namespace OmniSharp.MSBuild.Tests +{ + public class SolutionParsingTests + { + [Fact] + public void SolutionFile_Parse_throws_with_null_text() + { + Assert.Throws(() => SolutionFile.Parse(null)); + } + } +} From 685ff696f21de2b8a27581e8a43f675bd727e22a Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 18 Jul 2017 15:05:33 -0700 Subject: [PATCH 2/3] Flesh out solution parsing API and add tests --- .../SolutionParsing/GlobalSectionBlock.cs | 20 ++ .../InvalidSolutionFileException.cs | 19 ++ .../SolutionParsing/ProjectBlock.cs | 94 ++++++ .../SolutionParsing/ProjectSectionBlock.cs | 20 ++ .../SolutionParsing/Property.cs | 47 +++ .../SolutionParsing/Scanner.cs | 36 +++ .../SolutionParsing/SectionBlock.cs | 51 ++++ .../SolutionParsing/SolutionFile.cs | 110 ++++++- .../SolutionParsingTests.cs | 287 ++++++++++++++++++ 9 files changed, 682 insertions(+), 2 deletions(-) create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/GlobalSectionBlock.cs create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/InvalidSolutionFileException.cs create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/ProjectSectionBlock.cs create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/Property.cs create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/Scanner.cs create mode 100644 src/OmniSharp.MSBuild/SolutionParsing/SectionBlock.cs diff --git a/src/OmniSharp.MSBuild/SolutionParsing/GlobalSectionBlock.cs b/src/OmniSharp.MSBuild/SolutionParsing/GlobalSectionBlock.cs new file mode 100644 index 0000000000..5a7088620d --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/GlobalSectionBlock.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal sealed class GlobalSectionBlock : SectionBlock + { + private GlobalSectionBlock(string name, ImmutableArray properties) + : base(name, properties) + { + } + + public static GlobalSectionBlock Parse(string headerLine, Scanner scanner) + { + var (name, properties) = ParseNameAndProperties( + "GlobalSection", "EndGlobalSection", headerLine, scanner); + + return new GlobalSectionBlock(name, properties); + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/InvalidSolutionFileException.cs b/src/OmniSharp.MSBuild/SolutionParsing/InvalidSolutionFileException.cs new file mode 100644 index 0000000000..ed9410a7bd --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/InvalidSolutionFileException.cs @@ -0,0 +1,19 @@ +using System; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal class InvalidSolutionFileException : Exception + { + public InvalidSolutionFileException() + { + } + + public InvalidSolutionFileException(string message) : base(message) + { + } + + public InvalidSolutionFileException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs b/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs new file mode 100644 index 0000000000..796d4a9e18 --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Immutable; +using System.IO; +using System.Text.RegularExpressions; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal class ProjectBlock + { + // An example of a project line looks like this: + // Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{DEBCE986-61B9-435E-8018-44B9EF751655}" + private static readonly Lazy s_lazyProjectHeader = new Lazy( + () => new Regex + ( + "^" // Beginning of line + + "Project\\(\"(?.*)\"\\)" + + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace + + "\"(?.*)\"" + + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace + + "\"(?.*)\"" + + "\\s*,\\s*" // Any amount of whitespace plus "," plus any amount of whitespace + + "\"(?.*)\"" + + "$", // End-of-line + RegexOptions.Compiled) + ); + + public string ProjectTypeGuid { get; } + public string ProjectName { get; } + public string RelativePath { get; } + public string ProjectGuid { get; } + public ImmutableArray Sections { get; } + + private ProjectBlock(string projectTypeGuid, string projectName, string relativePath, string projectGuid, ImmutableArray sections) + { + ProjectTypeGuid = projectTypeGuid; + ProjectName = projectName; + RelativePath = relativePath; + ProjectGuid = projectGuid; + Sections = sections; + } + + public static ProjectBlock Parse(string headerLine, Scanner scanner) + { + var match = s_lazyProjectHeader.Value.Match(headerLine); + if (!match.Success) + { + return null; + } + + var projectTypeGuid = match.Groups["PROJECTTYPEGUID"].Value.Trim(); + var projectName = match.Groups["PROJECTNAME"].Value.Trim(); + var relativePath = match.Groups["RELATIVEPATH"].Value.Trim(); + var projectGuid = match.Groups["PROJECTGUID"].Value.Trim(); + + // If the project name is empty, set it to a generated generic value. + if (string.IsNullOrEmpty(projectName)) + { + projectName = "EmptyProjectName." + Guid.NewGuid(); + } + + if (relativePath.IndexOfAny(Path.GetInvalidPathChars()) >= 0) + { + throw new InvalidSolutionFileException("A project path contains an invalid character."); + } + + var sections = ImmutableArray.CreateBuilder(); + + // Search for project dependencies. Keep reading until we either... + // 1. reach the end of the file, + // 2. see "ProjectSection( at the beginning of the line, or + // 3. see "EndProject at the beginning of the line. + + string line; + while ((line = scanner.NextLine()) != null) + { + if (line == "EndProject") + { + break; + } + + if (line.StartsWith("ProjectSection(")) + { + var section = ProjectSectionBlock.Parse(line, scanner); + if (section != null) + { + sections.Add(section); + } + } + } + + return new ProjectBlock(projectTypeGuid, projectName, relativePath, projectGuid, sections.ToImmutable()); + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/ProjectSectionBlock.cs b/src/OmniSharp.MSBuild/SolutionParsing/ProjectSectionBlock.cs new file mode 100644 index 0000000000..0a74c1d15b --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/ProjectSectionBlock.cs @@ -0,0 +1,20 @@ +using System.Collections.Immutable; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal sealed class ProjectSectionBlock : SectionBlock + { + private ProjectSectionBlock(string name, ImmutableArray properties) + : base(name, properties) + { + } + + public static ProjectSectionBlock Parse(string headerLine, Scanner scanner) + { + var (name, properties) = ParseNameAndProperties( + "ProjectSection", "EndProjectSection", headerLine, scanner); + + return new ProjectSectionBlock(name, properties); + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/Property.cs b/src/OmniSharp.MSBuild/SolutionParsing/Property.cs new file mode 100644 index 0000000000..51217e4097 --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/Property.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.RegularExpressions; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal sealed class Property + { + // An example of a property line looks like this: + // AspNetCompiler.VirtualPath = "/webprecompile" + // Because website projects now include the target framework moniker as + // one of their properties, may have an '=' in it. + private static readonly Lazy s_lazyPropertyLine = new Lazy( + () => new Regex + ( + "^" // Beginning of line + + "(?[^=]*)" + + "\\s*=\\s*" // Any amount of whitespace plus "=" plus any amount of whitespace + + "(?.*)" + + "$", // End-of-line + RegexOptions.Compiled) + ); + + public string Name { get; } + public string Value { get; } + + private Property(string name, string value) + { + Name = name; + Value = value; + } + + public static Property Parse(string propertyLine) + { + var match = s_lazyPropertyLine.Value.Match(propertyLine); + + if (!match.Success) + { + return null; + } + + var name = match.Groups["PROPERTYNAME"].Value.Trim(); + var value = match.Groups["PROPERTYVALUE"].Value.Trim(); + + return new Property(name, value); + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/Scanner.cs b/src/OmniSharp.MSBuild/SolutionParsing/Scanner.cs new file mode 100644 index 0000000000..2470af7511 --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/Scanner.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal class Scanner : IDisposable + { + private readonly StringReader _reader; + private int _currentLineNumber; + + public Scanner(string text) + { + _reader = new StringReader(text); + } + + public void Dispose() + { + _reader.Dispose(); + } + + public string NextLine() + { + var line = _reader.ReadLine(); + + _currentLineNumber++; + + if (line != null) + { + line = line.Trim(); + } + + return line; + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/SectionBlock.cs b/src/OmniSharp.MSBuild/SolutionParsing/SectionBlock.cs new file mode 100644 index 0000000000..cd61cc3acf --- /dev/null +++ b/src/OmniSharp.MSBuild/SolutionParsing/SectionBlock.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Immutable; + +namespace OmniSharp.MSBuild.SolutionParsing +{ + internal abstract class SectionBlock + { + public string Name { get; } + public ImmutableArray Properties { get; } + + protected SectionBlock(string name, ImmutableArray properties) + { + Name = name; + Properties = properties; + } + + protected static (string name, ImmutableArray properties) ParseNameAndProperties( + string startSection, string endSection, string headerLine, Scanner scanner) + { + var startIndex = startSection.Length; + if (!startSection.EndsWith("(")) + { + startIndex++; + } + + var endIndex = headerLine.IndexOf(')', startIndex); + var name = endIndex >= startIndex + ? headerLine.Substring(startIndex, endIndex - startIndex) + : headerLine.Substring(startIndex); + + var properties = ImmutableArray.CreateBuilder(); + + string line; + while ((line = scanner.NextLine()) != null) + { + if (line.StartsWith(endSection, StringComparison.Ordinal)) + { + break; + } + + var property = Property.Parse(line); + if (property != null) + { + properties.Add(property); + } + } + + return (name, properties.ToImmutable()); + } + } +} diff --git a/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs b/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs index 1bca7fec01..053414e7fa 100644 --- a/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs +++ b/src/OmniSharp.MSBuild/SolutionParsing/SolutionFile.cs @@ -1,11 +1,26 @@ using System; +using System.Collections.Immutable; +using System.IO; namespace OmniSharp.MSBuild.SolutionParsing { internal sealed class SolutionFile { - private SolutionFile() + public Version FormatVersion { get; } + public Version VisualStudioVersion { get; } + public ImmutableArray Projects { get; } + public ImmutableArray GlobalSections { get; } + + private SolutionFile( + Version formatVersion, + Version visualStudioVersion, + ImmutableArray projects, + ImmutableArray globalSections) { + FormatVersion = formatVersion; + VisualStudioVersion = visualStudioVersion; + Projects = projects; + GlobalSections = globalSections; } public static SolutionFile Parse(string text) @@ -15,7 +30,98 @@ public static SolutionFile Parse(string text) throw new ArgumentNullException(nameof(text)); } - return new SolutionFile(); + using (var scanner = new Scanner(text)) + { + var formatVersion = ParseHeaderAndVersion(scanner); + + Version visualStudioVersion = null; + + var projects = ImmutableArray.CreateBuilder(); + var globalSections = ImmutableArray.CreateBuilder(); + + string line; + while ((line = scanner.NextLine()) != null) + { + if (line.StartsWith("Project(", StringComparison.Ordinal)) + { + var project = ProjectBlock.Parse(line, scanner); + if (project != null) + { + projects.Add(project); + } + } + else if (line.StartsWith("GlobalSection(", StringComparison.Ordinal)) + { + var globalSection = GlobalSectionBlock.Parse(line, scanner); + if (globalSection != null) + { + globalSections.Add(globalSection); + } + } + else if (line.StartsWith("VisualStudioVersion", StringComparison.Ordinal)) + { + visualStudioVersion = ParseVisualStudioVersion(line); + } + } + + return new SolutionFile(formatVersion, visualStudioVersion, projects.ToImmutable(), globalSections.ToImmutable()); + } + } + + public static SolutionFile ParseFile(string path) + { + var text = File.ReadAllText(path); + return Parse(text); + } + + private static Version ParseHeaderAndVersion(Scanner scanner) + { + const string HeaderPrefix = "Microsoft Visual Studio Solution File, Format Version "; + + // Read the file header. This can be on either of the first two lines. + for (var i = 0; i < 2; i++) + { + var line = scanner.NextLine(); + if (line == null) + { + break; + } + + if (line.StartsWith(HeaderPrefix, StringComparison.Ordinal)) + { + // Found the header. Now get the version. + var lineEnd = line.Substring(HeaderPrefix.Length); + + if (Version.TryParse(lineEnd, out var version)) + { + return version; + } + + return null; + } + } + + // If we got here, we didn't find the file header on either the first or second line. + throw new InvalidSolutionFileException("Solution header should be on first or second line."); + } + + private static Version ParseVisualStudioVersion(string line) + { + // The version line should look like: + // + // VisualStudioVersion = 15.0.26228.4 + + var tokens = line.Split(new[] { ' ', '=' }, StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length >= 2) + { + var versionText = tokens[1]; + if (Version.TryParse(versionText, out var result)) + { + return result; + } + } + + return null; } } } diff --git a/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs b/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs index 93960208ef..59e8f98347 100644 --- a/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs +++ b/tests/OmniSharp.MSBuild.Tests/SolutionParsingTests.cs @@ -6,10 +6,297 @@ namespace OmniSharp.MSBuild.Tests { public class SolutionParsingTests { + #region SimpleSolutionContent + private const string SimpleSolutionContent = @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project(""{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"") = ""ConsoleApplication1"", ""ConsoleApplication1\ConsoleApplication1.vbproj"", ""{AB3413A6-D689-486D-B7F0-A095371B3F13}"" + EndProject + Project(""{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"") = ""vbClassLibrary"", ""vbClassLibrary\vbClassLibrary.vbproj"", ""{BA333A76-4511-47B8-8DF4-CA51C303AD0B}"" + EndProject + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""ClassLibrary1"", ""ClassLibrary1\ClassLibrary1.csproj"", ""{DEBCE986-61B9-435E-8018-44B9EF751655}"" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {AB3413A6-D689-486D-B7F0-A095371B3F13}.Release|AnyCPU.Build.0 = Release|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {BA333A76-4511-47B8-8DF4-CA51C303AD0B}.Release|AnyCPU.Build.0 = Release|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {DEBCE986-61B9-435E-8018-44B9EF751655}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal"; + #endregion + #region SimpleSolutionWithDifferentSpacingContent + private const string SimpleSolutionWithDifferentSpacingContent = @" + Microsoft Visual Studio Solution File, Format Version 9.00 + # Visual Studio 2005 + Project("" { Project GUID} "") = "" Project name "", "" Relative path to project file "" , "" {0ABED153-9451-483C-8140-9E8D7306B216} "" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|AnyCPU = Debug|AnyCPU + Release|AnyCPU = Release|AnyCPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.ActiveCfg = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Debug|AnyCPU.Build.0 = Debug|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.ActiveCfg = Release|AnyCPU + {0ABED153-9451-483C-8140-9E8D7306B216}.Release|AnyCPU.Build.0 = Release|AnyCPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal"; + #endregion + #region UnitySolutionContent + private const string UnitySolutionContent = @" + Microsoft Visual Studio Solution File, Format Version 11.00 + # Visual Studio 2010 + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""LeopotamGroupLibrary"", ""Assembly-CSharp.csproj"", ""{0279C7A5-B8B1-345F-ED42-A58232A100B3}"" + EndProject + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""LeopotamGroupLibrary"", ""Assembly-CSharp-firstpass.csproj"", ""{CD80764A-B5E2-C644-F0D0-A85E486306D8}"" + EndProject + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""LeopotamGroupLibrary"", ""Assembly-CSharp-Editor.csproj"", ""{BEDD06D2-DCFB-A6D5-CAC1-1320A679D62A}"" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0279C7A5-B8B1-345F-ED42-A58232A100B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0279C7A5-B8B1-345F-ED42-A58232A100B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0279C7A5-B8B1-345F-ED42-A58232A100B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0279C7A5-B8B1-345F-ED42-A58232A100B3}.Release|Any CPU.Build.0 = Release|Any CPU + {CD80764A-B5E2-C644-F0D0-A85E486306D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD80764A-B5E2-C644-F0D0-A85E486306D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD80764A-B5E2-C644-F0D0-A85E486306D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD80764A-B5E2-C644-F0D0-A85E486306D8}.Release|Any CPU.Build.0 = Release|Any CPU + {BEDD06D2-DCFB-A6D5-CAC1-1320A679D62A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEDD06D2-DCFB-A6D5-CAC1-1320A679D62A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEDD06D2-DCFB-A6D5-CAC1-1320A679D62A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEDD06D2-DCFB-A6D5-CAC1-1320A679D62A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = Assembly-CSharp.csproj + EndGlobalSection + EndGlobal"; + #endregion + #region SolutionWithProjectSectionContent + private const string SolutionWithProjectSectionContent = @" + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio 15 + VisualStudioVersion = 15.0.26124.0 + MinimumVisualStudioVersion = 15.0.26124.0 + Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""src"", ""src"", ""{3C622F77-3C74-474E-AC38-7F30E9235F63}"" + EndProject + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""GG.Library.ConfigProvider.Vault"", ""src\GG.Library.ConfigProvider.Vault\GG.Library.ConfigProvider.Vault.csproj"", ""{1D50BF95-C9C0-4EF0-B869-0194684E8519}"" + EndProject + Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""build"", ""build"", ""{4CFA9523-BC33-4C49-BF8E-554943CDC653}"" + ProjectSection(SolutionItems) = preProject + build\Readme.txt = build\Readme.txt + EndProjectSection + EndProject + Project(""{2150E333-8FDC-42A3-9474-1A3956D46DE8}"") = ""test"", ""test"", ""{65E7B2FA-C1D0-411C-82D7-0DF418A16555}"" + EndProject + Project(""{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"") = ""GG.Library.ConfigProvider.Vault.Test"", ""test\GG.Library.ConfigProvider.Vault.Test\GG.Library.ConfigProvider.Vault.Test.csproj"", ""{0C768495-EA52-4703-AEAF-316A4C0A01CB}"" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Debug|x64.ActiveCfg = Debug|x64 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Debug|x64.Build.0 = Debug|x64 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Debug|x86.ActiveCfg = Debug|x86 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Debug|x86.Build.0 = Debug|x86 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Release|Any CPU.Build.0 = Release|Any CPU + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Release|x64.ActiveCfg = Release|x64 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Release|x64.Build.0 = Release|x64 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Release|x86.ActiveCfg = Release|x86 + {1D50BF95-C9C0-4EF0-B869-0194684E8519}.Release|x86.Build.0 = Release|x86 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Debug|x64.ActiveCfg = Debug|x64 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Debug|x64.Build.0 = Debug|x64 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Debug|x86.ActiveCfg = Debug|x86 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Debug|x86.Build.0 = Debug|x86 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Release|Any CPU.Build.0 = Release|Any CPU + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Release|x64.ActiveCfg = Release|x64 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Release|x64.Build.0 = Release|x64 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Release|x86.ActiveCfg = Release|x86 + {0C768495-EA52-4703-AEAF-316A4C0A01CB}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1D50BF95-C9C0-4EF0-B869-0194684E8519} = {3C622F77-3C74-474E-AC38-7F30E9235F63} + {0C768495-EA52-4703-AEAF-316A4C0A01CB} = {65E7B2FA-C1D0-411C-82D7-0DF418A16555} + EndGlobalSection + EndGlobal"; + #endregion + [Fact] public void SolutionFile_Parse_throws_with_null_text() { Assert.Throws(() => SolutionFile.Parse(null)); } + + [Fact] + public void SolutionFile_Parse_simple_solution() + { + var solution = SolutionFile.Parse(SimpleSolutionContent); + + Assert.NotNull(solution.FormatVersion); + Assert.Equal(new Version("9.00"), solution.FormatVersion); + + Assert.Null(solution.VisualStudioVersion); + + Assert.Equal(3, solution.Projects.Length); + + Assert.Equal("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}", solution.Projects[0].ProjectTypeGuid); + Assert.Equal("ConsoleApplication1", solution.Projects[0].ProjectName); + Assert.Equal(@"ConsoleApplication1\ConsoleApplication1.vbproj", solution.Projects[0].RelativePath); + Assert.Equal("{AB3413A6-D689-486D-B7F0-A095371B3F13}", solution.Projects[0].ProjectGuid); + + Assert.Equal("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}", solution.Projects[1].ProjectTypeGuid); + Assert.Equal("vbClassLibrary", solution.Projects[1].ProjectName); + Assert.Equal(@"vbClassLibrary\vbClassLibrary.vbproj", solution.Projects[1].RelativePath); + Assert.Equal("{BA333A76-4511-47B8-8DF4-CA51C303AD0B}", solution.Projects[1].ProjectGuid); + + Assert.Equal("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", solution.Projects[2].ProjectTypeGuid); + Assert.Equal("ClassLibrary1", solution.Projects[2].ProjectName); + Assert.Equal(@"ClassLibrary1\ClassLibrary1.csproj", solution.Projects[2].RelativePath); + Assert.Equal("{DEBCE986-61B9-435E-8018-44B9EF751655}", solution.Projects[2].ProjectGuid); + + Assert.Equal(3, solution.GlobalSections.Length); + Assert.Equal("SolutionConfigurationPlatforms", solution.GlobalSections[0].Name); + Assert.Equal("ProjectConfigurationPlatforms", solution.GlobalSections[1].Name); + Assert.Equal("SolutionProperties", solution.GlobalSections[2].Name); + } + + [Fact] + public void SolutionFile_Parse_simple_solution_with_different_spacing() + { + var solution = SolutionFile.Parse(SimpleSolutionWithDifferentSpacingContent); + + Assert.NotNull(solution.FormatVersion); + Assert.Equal(new Version("9.00"), solution.FormatVersion); + + Assert.Null(solution.VisualStudioVersion); + + Assert.Equal(1, solution.Projects.Length); + + Assert.Equal("{ Project GUID}", solution.Projects[0].ProjectTypeGuid); + Assert.Equal("Project name", solution.Projects[0].ProjectName); + Assert.Equal("Relative path to project file", solution.Projects[0].RelativePath); + Assert.Equal("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.Projects[0].ProjectGuid); + } + + [Fact] + public void SolutionFile_Parse_unity_solution() + { + var solution = SolutionFile.Parse(UnitySolutionContent); + + Assert.NotNull(solution.FormatVersion); + Assert.Equal(new Version("11.00"), solution.FormatVersion); + + Assert.Null(solution.VisualStudioVersion); + + Assert.Equal(3, solution.Projects.Length); + + Assert.Equal("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", solution.Projects[0].ProjectTypeGuid); + Assert.Equal("LeopotamGroupLibrary", solution.Projects[0].ProjectName); + Assert.Equal("Assembly-CSharp.csproj", solution.Projects[0].RelativePath); + Assert.Equal("{0279C7A5-B8B1-345F-ED42-A58232A100B3}", solution.Projects[0].ProjectGuid); + + Assert.Equal("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", solution.Projects[1].ProjectTypeGuid); + Assert.Equal("LeopotamGroupLibrary", solution.Projects[1].ProjectName); + Assert.Equal("Assembly-CSharp-firstpass.csproj", solution.Projects[1].RelativePath); + Assert.Equal("{CD80764A-B5E2-C644-F0D0-A85E486306D8}", solution.Projects[1].ProjectGuid); + + Assert.Equal("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", solution.Projects[2].ProjectTypeGuid); + Assert.Equal("LeopotamGroupLibrary", solution.Projects[2].ProjectName); + Assert.Equal("Assembly-CSharp-Editor.csproj", solution.Projects[2].RelativePath); + Assert.Equal("{BEDD06D2-DCFB-A6D5-CAC1-1320A679D62A}", solution.Projects[2].ProjectGuid); + + Assert.Equal(4, solution.GlobalSections.Length); + Assert.Equal("SolutionConfigurationPlatforms", solution.GlobalSections[0].Name); + Assert.Equal("ProjectConfigurationPlatforms", solution.GlobalSections[1].Name); + Assert.Equal("SolutionProperties", solution.GlobalSections[2].Name); + Assert.Equal("MonoDevelopProperties", solution.GlobalSections[3].Name); + } + + [Fact] + public void SolutionFile_Parse_solution_with_project_section() + { + var solution = SolutionFile.Parse(SolutionWithProjectSectionContent); + + Assert.NotNull(solution.FormatVersion); + Assert.Equal(new Version("12.00"), solution.FormatVersion); + + Assert.NotNull(solution.VisualStudioVersion); + Assert.Equal(new Version("15.0.26124.0"), solution.VisualStudioVersion); + + Assert.Equal(5, solution.Projects.Length); + + Assert.Equal("{2150E333-8FDC-42A3-9474-1A3956D46DE8}", solution.Projects[0].ProjectTypeGuid); + Assert.Equal("src", solution.Projects[0].ProjectName); + Assert.Equal("src", solution.Projects[0].RelativePath); + Assert.Equal("{3C622F77-3C74-474E-AC38-7F30E9235F63}", solution.Projects[0].ProjectGuid); + + Assert.Equal("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", solution.Projects[1].ProjectTypeGuid); + Assert.Equal("GG.Library.ConfigProvider.Vault", solution.Projects[1].ProjectName); + Assert.Equal(@"src\GG.Library.ConfigProvider.Vault\GG.Library.ConfigProvider.Vault.csproj", solution.Projects[1].RelativePath); + Assert.Equal("{1D50BF95-C9C0-4EF0-B869-0194684E8519}", solution.Projects[1].ProjectGuid); + + Assert.Equal("{2150E333-8FDC-42A3-9474-1A3956D46DE8}", solution.Projects[2].ProjectTypeGuid); + Assert.Equal("build", solution.Projects[2].ProjectName); + Assert.Equal("build", solution.Projects[2].RelativePath); + Assert.Equal("{4CFA9523-BC33-4C49-BF8E-554943CDC653}", solution.Projects[2].ProjectGuid); + + Assert.Equal("{2150E333-8FDC-42A3-9474-1A3956D46DE8}", solution.Projects[3].ProjectTypeGuid); + Assert.Equal("test", solution.Projects[3].ProjectName); + Assert.Equal("test", solution.Projects[3].RelativePath); + Assert.Equal("{65E7B2FA-C1D0-411C-82D7-0DF418A16555}", solution.Projects[3].ProjectGuid); + + Assert.Equal("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}", solution.Projects[4].ProjectTypeGuid); + Assert.Equal("GG.Library.ConfigProvider.Vault.Test", solution.Projects[4].ProjectName); + Assert.Equal(@"test\GG.Library.ConfigProvider.Vault.Test\GG.Library.ConfigProvider.Vault.Test.csproj", solution.Projects[4].RelativePath); + Assert.Equal("{0C768495-EA52-4703-AEAF-316A4C0A01CB}", solution.Projects[4].ProjectGuid); + + Assert.Equal(4, solution.GlobalSections.Length); + Assert.Equal("SolutionConfigurationPlatforms", solution.GlobalSections[0].Name); + Assert.Equal("SolutionProperties", solution.GlobalSections[1].Name); + Assert.Equal("ProjectConfigurationPlatforms", solution.GlobalSections[2].Name); + Assert.Equal("NestedProjects", solution.GlobalSections[3].Name); + } } } From 14f50f1da75b04e972f5c5e7004c5a0ae95eed52 Mon Sep 17 00:00:00 2001 From: Dustin Campbell Date: Tue, 18 Jul 2017 15:12:04 -0700 Subject: [PATCH 3/3] Use new solution parser in MSBuild project system --- src/OmniSharp.MSBuild/MSBuildProjectSystem.cs | 8 ++++---- src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs index e29af92ea2..d997bf8b46 100644 --- a/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs +++ b/src/OmniSharp.MSBuild/MSBuildProjectSystem.cs @@ -5,7 +5,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using Microsoft.Build.Construction; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.Configuration; @@ -18,6 +17,7 @@ using OmniSharp.MSBuild.Models.Events; using OmniSharp.MSBuild.ProjectFile; using OmniSharp.MSBuild.Resolution; +using OmniSharp.MSBuild.SolutionParsing; using OmniSharp.Options; using OmniSharp.Services; @@ -138,13 +138,13 @@ private IEnumerable GetProjectPathsFromSolution(string solutionFilePath) { _logger.LogInformation($"Detecting projects in '{solutionFilePath}'."); - var solutionFile = SolutionFile.Parse(solutionFilePath); + var solutionFile = SolutionFile.ParseFile(solutionFilePath); var processedProjects = new HashSet(StringComparer.OrdinalIgnoreCase); var result = new List(); - foreach (var project in solutionFile.ProjectsInOrder) + foreach (var project in solutionFile.Projects) { - if (project.ProjectType == SolutionProjectType.SolutionFolder) + if (project.IsSolutionFolder) { continue; } diff --git a/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs b/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs index 796d4a9e18..2cf2ddb8b7 100644 --- a/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs +++ b/src/OmniSharp.MSBuild/SolutionParsing/ProjectBlock.cs @@ -7,6 +7,8 @@ namespace OmniSharp.MSBuild.SolutionParsing { internal class ProjectBlock { + private const string SolutionFolderGuid = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"; + // An example of a project line looks like this: // Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClassLibrary1", "ClassLibrary1\ClassLibrary1.csproj", "{DEBCE986-61B9-435E-8018-44B9EF751655}" private static readonly Lazy s_lazyProjectHeader = new Lazy( @@ -30,6 +32,8 @@ internal class ProjectBlock public string ProjectGuid { get; } public ImmutableArray Sections { get; } + public bool IsSolutionFolder => ProjectTypeGuid.Equals(SolutionFolderGuid, StringComparison.OrdinalIgnoreCase); + private ProjectBlock(string projectTypeGuid, string projectName, string relativePath, string projectGuid, ImmutableArray sections) { ProjectTypeGuid = projectTypeGuid;