Skip to content

Commit b211668

Browse files
baronfeljeffkl
authored andcommitted
Use new serializer library to parse solution files (#6219)
1 parent 4d72121 commit b211668

File tree

15 files changed

+226
-103
lines changed

15 files changed

+226
-103
lines changed

Directory.Packages.props

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.Managed" Version="17.2.0-beta1-20502-01" />
7373
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.Managed.VS" Version="17.2.0-beta1-20502-01" />
7474
<PackageVersion Include="Microsoft.VisualStudio.ProjectSystem.VS" Version="17.4.221-pre" />
75+
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.28" />
7576
<!-- Microsoft.VisualStudio.SDK has vulnerable dependencies System.Text.json and Microsoft.IO.Redist. When it's upgraded, try removing the pinned packages -->
7677
<PackageVersion Include="Microsoft.VisualStudio.SDK" Version="17.10.40171" />
7778
<PackageVersion Include="Microsoft.VisualStudio.Sdk.TestFramework.Xunit" Version="17.6.32" />
@@ -192,6 +193,7 @@
192193
<_allowBuildFromSourcePackage Include="Microsoft.Extensions.CommandLineUtils.Sources" />
193194
<_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileProviders.Abstractions" />
194195
<_allowBuildFromSourcePackage Include="Microsoft.Extensions.FileSystemGlobbing" />
196+
<_allowBuildFromSourcePackage Include="Microsoft.VisualStudio.SolutionPersistence" />
195197
<_allowBuildFromSourcePackage Include="Microsoft.Web.Xdt" />
196198
<_allowBuildFromSourcePackage Include="Newtonsoft.Json" />
197199
<_allowBuildFromSourcePackage Include="System.Collections.Immutable" />

NuGet.Config

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<package pattern="Microsoft.*" />
3131
<package pattern="Microsoft.Build.Framework" />
3232
<package pattern="Microsoft.NET.StringTools" />
33+
<package pattern="Microsoft.VisualStudio.SolutionPersistence" />
3334
<package pattern="Microsoft.VisualStudio.TemplateWizardInterface" />
3435
<package pattern="moq" />
3536
<package pattern="MSTest.TestAdapter" />

eng/SourceBuildPrebuiltBaseline.xml

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<UsagePattern IdentityGlob="Microsoft.NETCore.Platforms/*" />
4747
<UsagePattern IdentityGlob="Microsoft.NETCore.Targets/*" />
4848
<UsagePattern IdentityGlob="Microsoft.VisualStudio.Setup.Configuration.Interop/*" />
49+
<UsagePattern IdentityGlob="Microsoft.VisualStudio.SolutionPersistence/*" />
4950
<UsagePattern IdentityGlob="Microsoft.Web.Xdt/*" />
5051
<UsagePattern IdentityGlob="Microsoft.Win32.Registry/*" />
5152
<UsagePattern IdentityGlob="Microsoft.Win32.SystemEvents/*" />

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/PackageReferenceCommands/ListPackage/ListPackageCommandRunner.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ public async Task<int> ExecuteCommandAsync(ListPackageArgs listPackageArgs)
5858

5959
//If the given file is a solution, get the list of projects
6060
//If not, then it's a project, which is put in a list
61-
var projectsPaths = Path.GetExtension(listPackageArgs.Path).Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) ?
62-
MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path).Where(f => File.Exists(f)) :
61+
var projectsPaths =
62+
(Path.GetExtension(listPackageArgs.Path).Equals(".sln", PathUtility.GetStringComparisonBasedOnOS()) ||
63+
Path.GetExtension(listPackageArgs.Path).Equals(".slnx", PathUtility.GetStringComparisonBasedOnOS())) ?
64+
(await MSBuildAPIUtility.GetProjectsFromSolution(listPackageArgs.Path, listPackageArgs.CancellationToken)).Where(f => File.Exists(f)) :
6365
new List<string>(new string[] { listPackageArgs.Path });
6466

6567
MSBuildAPIUtility msBuild = listPackageReportModel.MSBuildAPIUtility;

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommand.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.CommandLine;
77
using System.CommandLine.Help;
88
using System.IO;
9+
using System.Threading.Tasks;
910
using Microsoft.Extensions.CommandLineUtils;
1011

1112
namespace NuGet.CommandLine.XPlat.Commands.Why
@@ -35,7 +36,7 @@ public static void GetWhyCommand(CliCommand rootCommand)
3536
Register(rootCommand, CommandOutputLogger.Create, WhyCommandRunner.ExecuteCommand);
3637
}
3738

38-
internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> getLogger, Func<WhyCommandArgs, int> action)
39+
internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> getLogger, Func<WhyCommandArgs, Task<int>> action)
3940
{
4041
var whyCommand = new DocumentedCommand("why", Strings.WhyCommand_Description, "https://aka.ms/dotnet/nuget/why");
4142

@@ -84,7 +85,7 @@ internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> get
8485
whyCommand.Options.Add(frameworks);
8586
whyCommand.Options.Add(help);
8687

87-
whyCommand.SetAction((parseResult) =>
88+
whyCommand.SetAction(async (parseResult, cancellationToken) =>
8889
{
8990
ILoggerWithColor logger = getLogger();
9091

@@ -94,9 +95,10 @@ internal static void Register(CliCommand rootCommand, Func<ILoggerWithColor> get
9495
parseResult.GetValue(path),
9596
parseResult.GetValue(package),
9697
parseResult.GetValue(frameworks),
97-
logger);
98+
logger,
99+
cancellationToken);
98100

99-
int exitCode = action(whyCommandArgs);
101+
int exitCode = await action(whyCommandArgs);
100102
return exitCode;
101103
}
102104
catch (ArgumentException ex)

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandArgs.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System;
77
using System.Collections.Generic;
8+
using System.Threading;
89

910
namespace NuGet.CommandLine.XPlat.Commands.Why
1011
{
@@ -14,6 +15,7 @@ internal class WhyCommandArgs
1415
public string Package { get; }
1516
public List<string> Frameworks { get; }
1617
public ILoggerWithColor Logger { get; }
18+
public CancellationToken CancellationToken { get; }
1719

1820
/// <summary>
1921
/// A constructor for the arguments of the 'why' command.
@@ -22,16 +24,19 @@ internal class WhyCommandArgs
2224
/// <param name="package">The package for which we show the dependency graphs.</param>
2325
/// <param name="frameworks">The target framework(s) for which we show the dependency graphs.</param>
2426
/// <param name="logger"></param>
27+
/// <param name="cancellationToken"></param>
2528
public WhyCommandArgs(
2629
string path,
2730
string package,
2831
List<string> frameworks,
29-
ILoggerWithColor logger)
32+
ILoggerWithColor logger,
33+
CancellationToken cancellationToken)
3034
{
3135
Path = path ?? throw new ArgumentNullException(nameof(path));
3236
Package = package ?? throw new ArgumentNullException(nameof(package));
3337
Frameworks = frameworks ?? throw new ArgumentNullException(nameof(frameworks));
3438
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
39+
CancellationToken = cancellationToken;
3540
}
3641
}
3742
}

src/NuGet.Core/NuGet.CommandLine.XPlat/Commands/Why/WhyCommandRunner.cs

+9-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
using System.Collections.Generic;
88
using System.Globalization;
99
using System.IO;
10+
using System.Runtime.CompilerServices;
11+
using System.Threading;
12+
using System.Threading.Tasks;
1013
using Microsoft.Build.Evaluation;
1114
using NuGet.ProjectModel;
1215

@@ -20,7 +23,7 @@ internal static class WhyCommandRunner
2023
/// Executes the 'why' command.
2124
/// </summary>
2225
/// <param name="whyCommandArgs">CLI arguments for the 'why' command.</param>
23-
public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
26+
public static async Task<int> ExecuteCommand(WhyCommandArgs whyCommandArgs)
2427
{
2528
bool validArgumentsUsed = ValidatePathArgument(whyCommandArgs.Path, whyCommandArgs.Logger)
2629
&& ValidatePackageArgument(whyCommandArgs.Package, whyCommandArgs.Logger);
@@ -30,10 +33,10 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
3033
}
3134

3235
string targetPackage = whyCommandArgs.Package;
33-
IEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles;
36+
IAsyncEnumerable<(string assetsFilePath, string? projectPath)> assetsFiles;
3437
try
3538
{
36-
assetsFiles = FindAssetsFiles(whyCommandArgs.Path, whyCommandArgs.Logger);
39+
assetsFiles = FindAssetsFilesAsync(whyCommandArgs.Path, whyCommandArgs.Logger, whyCommandArgs.CancellationToken);
3740
}
3841
catch (ArgumentException ex)
3942
{
@@ -47,7 +50,7 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
4750
}
4851

4952
bool anyErrors = false;
50-
foreach ((string assetsFilePath, string? projectPath) in assetsFiles)
53+
await foreach ((string assetsFilePath, string? projectPath) in assetsFiles)
5154
{
5255
LockFile? assetsFile = GetProjectAssetsFile(assetsFilePath, projectPath, whyCommandArgs.Logger);
5356

@@ -88,15 +91,15 @@ public static int ExecuteCommand(WhyCommandArgs whyCommandArgs)
8891
return anyErrors ? ExitCodes.Error : ExitCodes.Success;
8992
}
9093

91-
private static IEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFiles(string path, ILoggerWithColor logger)
94+
private static async IAsyncEnumerable<(string assetsFilePath, string? projectPath)> FindAssetsFilesAsync(string path, ILoggerWithColor logger, [EnumeratorCancellation] CancellationToken cancellationToken = default)
9295
{
9396
if (XPlatUtility.IsJsonFile(path))
9497
{
9598
yield return (path, null);
9699
yield break;
97100
}
98101

99-
var projectPaths = MSBuildAPIUtility.GetListOfProjectsFromPathArgument(path);
102+
var projectPaths = await MSBuildAPIUtility.GetListOfProjectsFromPathArgumentAsync(path, cancellationToken);
100103
foreach (string projectPath in projectPaths.NoAllocEnumerate())
101104
{
102105
Project project = MSBuildAPIUtility.GetProject(projectPath);

src/NuGet.Core/NuGet.CommandLine.XPlat/NuGet.CommandLine.XPlat.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" PrivateAssets="All" />
1919
<PackageReference Include="Microsoft.Build" ExcludeAssets="runtime" />
2020
<PackageReference Include="System.CommandLine" />
21+
<PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
2122
</ItemGroup>
2223

2324
<!-- Microsoft.Build.Locator is needed when debugging, but should not be used in the assemblies we insert. -->

src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/MSBuildAPIUtility.cs

+11-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
using System.IO;
88
using System.Linq;
99
using System.Text;
10+
using System.Threading;
11+
using System.Threading.Tasks;
1012
using Microsoft.Build.Construction;
1113
using Microsoft.Build.Evaluation;
1214
using Microsoft.Build.Execution;
15+
using Microsoft.VisualStudio.SolutionPersistence;
16+
using Microsoft.VisualStudio.SolutionPersistence.Model;
17+
using Microsoft.VisualStudio.SolutionPersistence.Serializer;
1318
using NuGet.Common;
1419
using NuGet.Frameworks;
1520
using NuGet.LibraryModel;
@@ -79,18 +84,19 @@ private static Project GetProject(string projectCSProjPath, IDictionary<string,
7984
return new Project(projectRootElement, globalProperties, toolsVersion: null);
8085
}
8186

82-
internal static IEnumerable<string> GetProjectsFromSolution(string solutionPath)
87+
internal static async Task<IEnumerable<string>> GetProjectsFromSolution(string solutionPath, CancellationToken cancellationToken = default)
8388
{
84-
var sln = SolutionFile.Parse(solutionPath);
85-
return sln.ProjectsInOrder.Select(p => p.AbsolutePath);
89+
ISolutionSerializer serializer = SolutionSerializers.GetSerializerByMoniker(solutionPath);
90+
SolutionModel solution = await serializer.OpenAsync(solutionPath, cancellationToken);
91+
return solution.SolutionProjects.Select(p => p.FilePath);
8692
}
8793

8894
/// <summary>
8995
/// Get the list of project paths from the input 'path' argument. Path must be a directory, solution file or project file.
9096
/// </summary>
9197
/// <returns>List of project paths. Returns null if path was a directory with none or multiple project/solution files.</returns>
9298
/// <exception cref="ArgumentException">Throws an exception if the directory has none or multiple project/solution files.</exception>
93-
internal static IEnumerable<string> GetListOfProjectsFromPathArgument(string path)
99+
internal static async Task<IEnumerable<string>> GetListOfProjectsFromPathArgumentAsync(string path, CancellationToken cancellationToken = default)
94100
{
95101
string fullPath = Path.GetFullPath(path);
96102

@@ -116,7 +122,7 @@ internal static IEnumerable<string> GetListOfProjectsFromPathArgument(string pat
116122
}
117123

118124
return XPlatUtility.IsSolutionFile(projectOrSolutionFile)
119-
? MSBuildAPIUtility.GetProjectsFromSolution(projectOrSolutionFile).Where(f => File.Exists(f))
125+
? (await MSBuildAPIUtility.GetProjectsFromSolution(projectOrSolutionFile, cancellationToken)).Where(f => File.Exists(f))
120126
: [projectOrSolutionFile];
121127
}
122128

src/NuGet.Core/NuGet.CommandLine.XPlat/Utility/XPlatUtility.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ internal static bool IsSolutionFile(string fileName)
132132
{
133133
var extension = System.IO.Path.GetExtension(fileName);
134134

135-
return string.Equals(extension, ".sln", StringComparison.OrdinalIgnoreCase);
135+
return string.Equals(extension, ".sln", StringComparison.OrdinalIgnoreCase) || string.Equals(extension, ".slnx", StringComparison.OrdinalIgnoreCase);
136136
}
137137

138138
return false;

test/NuGet.Core.FuncTests/NuGet.XPlat.FuncTest/ListPackageTests.cs

+65-8
Original file line numberDiff line numberDiff line change
@@ -261,17 +261,17 @@ static void SetupCredentialServiceMock(Mock<ICredentialService> mockedCredential
261261
return outCredentials != null;
262262
});
263263
}
264+
}
264265

265-
static async Task RestoreProjectsAsync(SimpleTestPathContext pathContext, SimpleTestProjectContext projectA, SimpleTestProjectContext projectB, ITestOutputHelper testOutputHelper)
266-
{
267-
var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.SolutionRoot), Path.GetFileName(pathContext.NuGetConfig), null);
268-
var packageSourceProvider = new PackageSourceProvider(settings);
266+
static async Task RestoreProjectsAsync(SimpleTestPathContext pathContext, SimpleTestProjectContext projectA, SimpleTestProjectContext projectB, ITestOutputHelper testOutputHelper)
267+
{
268+
var settings = Settings.LoadDefaultSettings(Path.GetDirectoryName(pathContext.SolutionRoot), Path.GetFileName(pathContext.NuGetConfig), null);
269+
var packageSourceProvider = new PackageSourceProvider(settings);
269270

270-
var sources = packageSourceProvider.LoadPackageSources();
271+
var sources = packageSourceProvider.LoadPackageSources();
271272

272-
await RestoreProjectAsync(settings, pathContext, projectA, sources, testOutputHelper);
273-
await RestoreProjectAsync(settings, pathContext, projectB, sources, testOutputHelper);
274-
}
273+
await RestoreProjectAsync(settings, pathContext, projectA, sources, testOutputHelper);
274+
await RestoreProjectAsync(settings, pathContext, projectB, sources, testOutputHelper);
275275

276276
static async Task RestoreProjectAsync(ISettings settings,
277277
SimpleTestPathContext pathContext,
@@ -290,6 +290,63 @@ static async Task RestoreProjectAsync(ISettings settings,
290290
}
291291
}
292292

293+
[InlineData(true)]
294+
[InlineData(false)]
295+
[Theory]
296+
public async Task CanListPackagesForProjectsInSolutions(bool useSlnx)
297+
{
298+
// Arrange
299+
using var pathContext = new SimpleTestPathContext();
300+
301+
var packageA100 = new SimpleTestPackageContext("A", "1.0.0");
302+
var packageB100 = new SimpleTestPackageContext("B", "1.0.0");
303+
304+
await SimpleTestPackageUtility.CreatePackagesAsync(
305+
pathContext.PackageSource,
306+
packageA100,
307+
packageB100);
308+
309+
var projectA = SimpleTestProjectContext.CreateNETCore("ProjectA", pathContext.SolutionRoot, "net6.0");
310+
var projectB = SimpleTestProjectContext.CreateNETCore("ProjectB", pathContext.SolutionRoot, "net6.0");
311+
312+
projectA.AddPackageToAllFrameworks(packageA100);
313+
projectB.AddPackageToAllFrameworks(packageB100);
314+
315+
var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot, useSlnx);
316+
solution.Projects.Add(projectA);
317+
solution.Projects.Add(projectB);
318+
solution.Create(pathContext.SolutionRoot);
319+
320+
using var mockServer = new FileSystemBackedV3MockServer(pathContext.PackageSource, isPrivateFeed: true);
321+
mockServer.Start();
322+
pathContext.Settings.AddSource(sourceName: "private-source", sourceUri: mockServer.ServiceIndexUri, allowInsecureConnectionsValue: bool.TrueString);
323+
324+
// List package command requires restore to be run before it can list packages.
325+
await RestoreProjectsAsync(pathContext, projectA, projectB, _testOutputHelper);
326+
327+
var output = new StringBuilder();
328+
var error = new StringBuilder();
329+
using TextWriter consoleOut = new StringWriter(output);
330+
using TextWriter consoleError = new StringWriter(error);
331+
var logger = new TestLogger(_testOutputHelper);
332+
ListPackageCommandRunner listPackageCommandRunner = new();
333+
var packageRefArgs = new ListPackageArgs(
334+
path: solution.SolutionPath,
335+
packageSources: [new(mockServer.ServiceIndexUri)],
336+
frameworks: ["net6.0"],
337+
reportType: ReportType.Vulnerable,
338+
renderer: new ListPackageConsoleRenderer(consoleOut, consoleError),
339+
includeTransitive: false,
340+
prerelease: false,
341+
highestPatch: false,
342+
highestMinor: false,
343+
logger: logger,
344+
cancellationToken: CancellationToken.None);
345+
346+
int result = await listPackageCommandRunner.ExecuteCommandAsync(packageRefArgs);
347+
Assert.True(result == 0, userMessage: logger.ShowMessages());
348+
}
349+
293350
private void VerifyCommand(Action<string, Mock<IListPackageCommandRunner>, CommandLineApplication, Func<LogLevel>> verify)
294351
{
295352
// Arrange

0 commit comments

Comments
 (0)