Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opt-in .sln parsing with Microsoft.VisualStudio.SolutionPersistence #11538

Merged
merged 16 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions eng/BootStrapMsBuild.targets
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
<_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Newtonsoft.Json'))' == 'true'" />
<_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('NuGetSdkResolver'))' == 'true'" />
<_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Microsoft.Extensions.'))' == 'true'" />

<_NuGetRuntimeDependencies Include="%(RuntimeCopyLocalItems.Identity)" Condition="'@(RuntimeCopyLocalItems->Contains('Microsoft.VisualStudio.SolutionPersistence'))' == 'true'" />

<!-- NuGet.targets and NuGet.RestoreEx.targets will be in the RuntimeTargetsCopyLocalItems ItemGroup -->
<_NuGetRuntimeDependencies Include="%(RuntimeTargetsCopyLocalItems.Identity)" Condition="'@(RuntimeTargetsCopyLocalItems->Contains('NuGet.'))' == 'true'" />

Expand All @@ -48,7 +49,7 @@

<Target Name="RemoveExtraAssemblyReferences" BeforeTargets="ResolveAssemblyReferences">
<!-- This is really hacky, but these references will cause issues when trying to 'build' this project.
To acquire the NuGet binaries we depend on for local run-time ('bootstrap'), we we are using a PackageReference (to
To acquire the NuGet binaries we depend on for local run-time ('bootstrap'), we are using a PackageReference (to
'NuGet.Build.Tasks' and 'Microsoft.Build.NuGetSdkResolver'). This has the advantage of using NuGets compatibility
check to ensure we choose the right version of those assemblies. But, at 'bootstrap' time these runtime dependencies
need to be in a specific location that does not mesh with NuGet. To resolve this, we include the default
Expand Down
797 changes: 141 additions & 656 deletions src/Build.OM.UnitTests/Construction/SolutionFile_Tests.cs

Large diffs are not rendered by default.

20 changes: 12 additions & 8 deletions src/Build.UnitTests/Construction/SolutionFile_NewParser_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,23 @@ public void ProjectWithWebsiteProperties(bool convertToSlnx)
internal static SolutionFile ParseSolutionHelper(string solutionFileContents, bool convertToSlnx = false)
{
solutionFileContents = solutionFileContents.Replace('\'', '"');

using (TestEnvironment testEnvironment = TestEnvironment.Create())
{
TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents);

string solutionPath = convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path;

SolutionFile solutionFile = new SolutionFile { FullPath = solutionPath };
solutionFile.ParseUsingNewParser();
return solutionFile;
return ParseSolutionHelper(testEnvironment, solutionFileContents, convertToSlnx);
}
}

internal static SolutionFile ParseSolutionHelper(TestEnvironment testEnvironment, string solutionFileContents, bool convertToSlnx = false)
{
solutionFileContents = solutionFileContents.Replace('\'', '"');
testEnvironment.SetEnvironmentVariable("MSBUILD_PARSE_SLN_WITH_SOLUTIONPERSISTENCE", "1");
TransientTestFile sln = testEnvironment.CreateFile(FileUtilities.GetTemporaryFileName(".sln"), solutionFileContents);
string solutionPath = convertToSlnx ? ConvertToSlnx(sln.Path) : sln.Path;
SolutionFile solutionFile = new SolutionFile { FullPath = solutionPath };
solutionFile.ParseUsingNewParser();
return solutionFile;
}

private static string ConvertToSlnx(string slnPath)
{
string slnxPath = slnPath + "x";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Build.Construction;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Shared;
Expand All @@ -16,11 +17,11 @@

namespace Microsoft.Build.UnitTests.Construction
{
public class SolutionFile_Tests
public class SolutionFile_OldParser_Tests
{
public ITestOutputHelper TestOutputHelper { get; }

public SolutionFile_Tests(ITestOutputHelper testOutputHelper)
public SolutionFile_OldParser_Tests(ITestOutputHelper testOutputHelper)
{
TestOutputHelper = testOutputHelper;
}
Expand Down Expand Up @@ -104,6 +105,42 @@ public void ParseFirstProjectLineWithDifferentSpacing()
proj.ProjectGuid.ShouldBe("Unique name-GUID");
}

/// <summary>
/// A slightly more complicated test where there is some different whitespace.
/// </summary>
[Fact]
public void ParseSolutionWithDifferentSpacing()
{
string solutionFileContents =
@"
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
";

SolutionFile solution = ParseSolutionHelper(solutionFileContents);

Assert.Equal("Project name", solution.ProjectsInOrder[0].ProjectName);
Assert.Equal("Relative path to project file", solution.ProjectsInOrder[0].RelativePath);
Assert.Equal("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid);
}

/// <summary>
/// First project line with an empty project name. This is somewhat malformed, but we should
/// still behave reasonably instead of crashing.
Expand Down Expand Up @@ -687,6 +724,43 @@ public void ParseFirstProjectLineWhereProjectNameHasSpecialCharacters()
proj.ProjectGuid.ShouldBe("Unique name-GUID");
}

/// <summary>
/// Test some characters that are valid in a file name but that also could be
/// considered a delimiter by a parser. Does quoting work for special characters?
/// </summary>
[Fact]
public void ParseSolutionWhereProjectNameHasSpecialCharacters()
{
string solutionFileContents =
@"
Microsoft Visual Studio Solution File, Format Version 9.00
# Visual Studio 2005
Project('{Project GUID}') = 'MyProject,(=IsGreat)', '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
";

SolutionFile solution = ParseSolutionHelper(solutionFileContents);

Assert.Equal("MyProject,(=IsGreat)", solution.ProjectsInOrder[0].ProjectName);
Assert.Equal("Relative path to project file", solution.ProjectsInOrder[0].RelativePath);
Assert.Equal("{0ABED153-9451-483C-8140-9E8D7306B216}", solution.ProjectsInOrder[0].ProjectGuid);
}

/// <summary>
/// Test some characters that are valid in a file name but that also could be
/// considered a delimiter by a parser. Does quoting work for special characters?
Expand Down Expand Up @@ -2355,5 +2429,58 @@ public void ParseSolutionWithParentedPaths()
solution.ProjectsInOrder[0].AbsolutePath.ShouldBe(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(solution.FullPath)!, expectedRelativePath)));
solution.ProjectsInOrder[0].ProjectGuid.ShouldBe("{0ABED153-9451-483C-8140-9E8D7306B216}");
}

/// <summary>
/// Parse solution file with comments
/// </summary>
[Fact]
public void ParseSolutionWithComments()
{
const string solutionFileContent = @"
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29123.89
MinimumVisualStudioVersion = 10.0.40219.1
Project('{9A19103F-16F7-4668-BE54-9A1E7A4F7556}') = 'SlnCommentTest', 'SlnCommentTest.csproj', '{00000000-0000-0000-FFFF-FFFFFFFFFFFF}'
EndProject
Project('{2150E333-8FDC-42A3-9474-1A3956D46DE8}') = 'Solution Items', 'Solution Items', '{054DED3B-B890-4652-B449-839F581E5D86}'
ProjectSection(SolutionItems) = preProject
SlnFile.txt = SlnFile.txt
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{00000000-0000-0000-FFFF-FFFFFFFFFFFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00000000-0000-0000-FFFF-FFFFFFFFFFFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00000000-0000-0000-FFFF-FFFFFFFFFFFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00000000-0000-0000-FFFF-FFFFFFFFFFFF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FFFFFFFF-FFFF-FFFF-0000-000000000000}
EndGlobalSection
EndGlobal
";

StringBuilder stringBuilder = new StringBuilder();

// Put comment between all lines
const string comment = "\t# comment";
string[] lines = solutionFileContent.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < lines.Length; i++)
{
stringBuilder.AppendLine(comment);
stringBuilder.AppendLine(lines[i]);
}
stringBuilder.AppendLine(comment);

Should.NotThrow(() => ParseSolutionHelper(stringBuilder.ToString()));
}
}
}
8 changes: 4 additions & 4 deletions src/Build.UnitTests/Construction/SolutionFilter_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ public void SolutionFilterFiltersProjects(bool graphBuild)
");
// Slashes here (and in the .slnf) are hardcoded as backslashes intentionally to support the common case.
TransientTestFile solutionFile = testEnvironment.CreateFile(simpleProjectFolder, "SimpleProject.sln",
@"
"""
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29326.124
MinimumVisualStudioVersion = 10.0.40219.1
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""SimpleProject"", ""SimpleProject\SimpleProject.csproj"", ""{79B5EBA6-5D27-4976-BC31-14422245A59A}""
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleProject", "SimpleProject\SimpleProject.csproj", "{79B5EBA6-5D27-4976-BC31-14422245A59A}"
EndProject
Project(""{9A19103F-16F7-4668-BE54-9A1E7A4F7556}"") = ""ClassLibrary"", ""..\ClassLibrary\ClassLibrary\ClassLibrary.csproj"", ""{8EFCCA22-9D51-4268-90F7-A595E11FCB2D}""
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClassLibrary", "..\ClassLibrary\ClassLibrary\ClassLibrary.csproj", "{8EFCCA22-9D51-4268-90F7-A595E11FCB2D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -107,7 +107,7 @@ public void SolutionFilterFiltersProjects(bool graphBuild)
SolutionGuid = {DE7234EC-0C4D-4070-B66A-DCF1B4F0CFEF}
EndGlobalSection
EndGlobal
");
""");
TransientTestFile filterFile = testEnvironment.CreateFile(folder, "solutionFilter.slnf",
/*lang=json*/
"""
Expand Down
Loading
Loading