Skip to content

Commit 16870d3

Browse files
authored
Merge pull request #1338 from dotnet/jnm2/tools3
25x nonparallelized speedup by avoiding MSBuildWorkspace in example tester
2 parents 295a844 + 09ec8a4 commit 16870d3

File tree

8 files changed

+500
-10
lines changed

8 files changed

+500
-10
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[assembly: Parallelizable(ParallelScope.Children)]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
11+
<PackageReference Include="NUnit" Version="4.3.2" />
12+
<PackageReference Include="NUnit.Analyzers" Version="4.8.1" PrivateAssets="all" />
13+
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
14+
<PackageReference Include="Shouldly" Version="4.3.0" />
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<ProjectReference Include="..\ExampleTester\ExampleTester.csproj" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<Using Include="NUnit.Framework" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
using System.Collections.Immutable;
2+
using System.Xml.Linq;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
5+
using Microsoft.CodeAnalysis.MSBuild;
6+
using Shouldly;
7+
8+
namespace ExampleTester.Tests;
9+
10+
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
11+
12+
public static class FastCsprojCompilationParserTests
13+
{
14+
private const string CsprojFileName = "Test.csproj";
15+
16+
private static CsprojParseResult ParseCsproj(string csprojContents)
17+
{
18+
var result = FastCsprojCompilationParser.ParseCsproj(XDocument.Parse(csprojContents), CsprojFileName);
19+
CompareMSBuildWorkspaceCompilation(csprojContents, result);
20+
return result;
21+
}
22+
23+
private static void CompareMSBuildWorkspaceCompilation(string csprojContents, CsprojParseResult result)
24+
{
25+
var msbuildCompilation = GetMSBuildWorkspaceCompilation(csprojContents);
26+
27+
result.AssemblyName.ShouldBe(msbuildCompilation.AssemblyName);
28+
29+
var sanitizedCompilationOptions = msbuildCompilation.Options
30+
.WithAssemblyIdentityComparer(result.CompilationOptions.AssemblyIdentityComparer)
31+
.WithMetadataReferenceResolver(null)
32+
.WithSourceReferenceResolver(null)
33+
.WithStrongNameProvider(null)
34+
.WithSyntaxTreeOptionsProvider(null)
35+
.WithXmlReferenceResolver(null);
36+
37+
result.CompilationOptions.Equals(sanitizedCompilationOptions).ShouldBeTrue();
38+
39+
var sanitizedParseOptions = msbuildCompilation.SyntaxTrees.First().Options;
40+
41+
result.ParseOptions.Equals(sanitizedParseOptions).ShouldBeTrue();
42+
43+
var sanitizedSyntaxTrees = msbuildCompilation.SyntaxTrees
44+
.Where(tree => !new[] { ".AssemblyAttributes.cs", ".AssemblyInfo.cs" }.Any(ending =>
45+
Path.GetFileName(tree.FilePath).EndsWith(ending, StringComparison.OrdinalIgnoreCase)))
46+
.ToImmutableArray();
47+
48+
result.GeneratedSources.SequenceEqual(sanitizedSyntaxTrees, (a, b) =>
49+
Path.GetFileName(a.FilePath).Equals(Path.GetFileName(b.FilePath), StringComparison.OrdinalIgnoreCase)
50+
&& a.Options.Equals(b.Options)
51+
&& a.GetText().ToString() == b.GetText().ToString())
52+
.ShouldBeTrue();
53+
}
54+
55+
private static Compilation GetMSBuildWorkspaceCompilation(string csprojContents)
56+
{
57+
using var workspace = MSBuildWorkspace.Create(new Dictionary<string, string> { ["Configuration"] = "Release" });
58+
59+
var tempFolder = Path.Join(Path.GetTempPath(), Path.GetRandomFileName());
60+
Directory.CreateDirectory(tempFolder);
61+
try
62+
{
63+
var csprojPath = Path.Join(tempFolder, CsprojFileName);
64+
File.WriteAllText(csprojPath, csprojContents);
65+
var project = workspace.OpenProjectAsync(csprojPath).GetAwaiter().GetResult();
66+
return project.GetCompilationAsync().GetAwaiter().GetResult().ShouldNotBeNull();
67+
}
68+
finally
69+
{
70+
Directory.Delete(tempFolder, recursive: true);
71+
}
72+
}
73+
74+
[Test]
75+
public static void Defaults()
76+
{
77+
var result = ParseCsproj("""
78+
<Project Sdk="Microsoft.NET.Sdk">
79+
80+
<PropertyGroup>
81+
<TargetFramework>net6.0</TargetFramework>
82+
</PropertyGroup>
83+
84+
</Project>
85+
""");
86+
87+
result.CompilationOptions.OutputKind.ShouldBe(OutputKind.DynamicallyLinkedLibrary);
88+
result.CompilationOptions.NullableContextOptions.ShouldBe(NullableContextOptions.Disable);
89+
result.AssemblyName.ShouldBe(Path.GetFileNameWithoutExtension(CsprojFileName));
90+
result.CompilationOptions.AllowUnsafe.ShouldBeFalse();
91+
result.ParseOptions.LanguageVersion.ShouldBe(LanguageVersion.CSharp10); // Due to net6.0
92+
result.CompilationOptions.WarningLevel.ShouldBe(6); // Due to net6.0
93+
result.GeneratedSources.ShouldBeEmpty();
94+
}
95+
96+
[Test]
97+
public static void ParsesTargetFramework()
98+
{
99+
ParseCsproj("""
100+
<Project Sdk="Microsoft.NET.Sdk">
101+
102+
<PropertyGroup>
103+
<TargetFramework>net6.0</TargetFramework>
104+
</PropertyGroup>
105+
106+
</Project>
107+
""").TargetFramework.ShouldBe("net6.0");
108+
}
109+
110+
[Test]
111+
public static void ParsesOutputType([Values("Library", "Exe", "WinExe")] string outputType)
112+
{
113+
ParseCsproj($"""
114+
<Project Sdk="Microsoft.NET.Sdk">
115+
116+
<PropertyGroup>
117+
<TargetFramework>net6.0</TargetFramework>
118+
<OutputType>{outputType}</OutputType>
119+
</PropertyGroup>
120+
121+
</Project>
122+
""").CompilationOptions.OutputKind.ShouldBe(outputType switch
123+
{
124+
"Library" => OutputKind.DynamicallyLinkedLibrary,
125+
"Exe" => OutputKind.ConsoleApplication,
126+
"WinExe" => OutputKind.WindowsApplication,
127+
});
128+
}
129+
130+
[Test]
131+
public static void ParsesNullable([Values("enable", "disable", "annotations", "warnings")] string nullable)
132+
{
133+
ParseCsproj($"""
134+
<Project Sdk="Microsoft.NET.Sdk">
135+
136+
<PropertyGroup>
137+
<TargetFramework>net6.0</TargetFramework>
138+
<Nullable>{nullable}</Nullable>
139+
</PropertyGroup>
140+
141+
</Project>
142+
""").CompilationOptions.NullableContextOptions.ShouldBe(nullable switch
143+
{
144+
"enable" => NullableContextOptions.Enable,
145+
"disable" => NullableContextOptions.Disable,
146+
"annotations" => NullableContextOptions.Annotations,
147+
"warnings" => NullableContextOptions.Warnings,
148+
});
149+
}
150+
151+
[Test]
152+
public static void ParsesAssemblyName()
153+
{
154+
ParseCsproj($"""
155+
<Project Sdk="Microsoft.NET.Sdk">
156+
157+
<PropertyGroup>
158+
<TargetFramework>net6.0</TargetFramework>
159+
<AssemblyName>Xyz</AssemblyName>
160+
</PropertyGroup>
161+
162+
</Project>
163+
""").AssemblyName.ShouldBe("Xyz");
164+
}
165+
166+
[Test]
167+
public static void ParsesAllowUnsafeBlocks([Values("true", "false")] string allowUnsafeBlocks)
168+
{
169+
ParseCsproj($"""
170+
<Project Sdk="Microsoft.NET.Sdk">
171+
172+
<PropertyGroup>
173+
<TargetFramework>net6.0</TargetFramework>
174+
<AllowUnsafeBlocks>{allowUnsafeBlocks}</AllowUnsafeBlocks>
175+
</PropertyGroup>
176+
177+
</Project>
178+
""").CompilationOptions.AllowUnsafe.ShouldBe(allowUnsafeBlocks switch
179+
{
180+
"true" => true,
181+
"false" => false,
182+
});
183+
}
184+
185+
[Test]
186+
public static void ParsesImplicitUsings([Values("true", "enable", "false", "disable")] string implicitUsings)
187+
{
188+
var hasImplicitUsings = implicitUsings switch
189+
{
190+
"true" or "enable" => true,
191+
"false" or "disable" => false,
192+
};
193+
194+
var result = ParseCsproj($"""
195+
<Project Sdk="Microsoft.NET.Sdk">
196+
197+
<PropertyGroup>
198+
<TargetFramework>net6.0</TargetFramework>
199+
<ImplicitUsings>{implicitUsings}</ImplicitUsings>
200+
</PropertyGroup>
201+
202+
</Project>
203+
""");
204+
205+
if (hasImplicitUsings)
206+
{
207+
var source = result.GeneratedSources.ShouldHaveSingleItem();
208+
source.Options.ShouldBe(result.ParseOptions);
209+
source.FilePath.ShouldBe("Test.GlobalUsings.g.cs");
210+
source.GetText().ToString().ShouldBe("""
211+
// <auto-generated/>
212+
global using global::System;
213+
global using global::System.Collections.Generic;
214+
global using global::System.IO;
215+
global using global::System.Linq;
216+
global using global::System.Net.Http;
217+
global using global::System.Threading;
218+
global using global::System.Threading.Tasks;
219+
220+
""");
221+
}
222+
else
223+
{
224+
result.GeneratedSources.ShouldBeEmpty();
225+
}
226+
}
227+
228+
[Test]
229+
public static void ThrowsNotImplementedExceptionForUnrecognizedProperty()
230+
{
231+
Should.Throw<NotImplementedException>(() => ParseCsproj("""
232+
<Project Sdk="Microsoft.NET.Sdk">
233+
234+
<PropertyGroup>
235+
<Xyz></Xyz>
236+
</PropertyGroup>
237+
238+
</Project>
239+
"""));
240+
}
241+
242+
[Test]
243+
public static void ThrowsNotImplementedExceptionForUnrecognizedItem()
244+
{
245+
Should.Throw<NotImplementedException>(() => ParseCsproj("""
246+
<Project Sdk="Microsoft.NET.Sdk">
247+
248+
<ItemGroup>
249+
<Xyz Include="" />
250+
</ItemGroup>
251+
252+
</Project>
253+
"""));
254+
}
255+
256+
[Test]
257+
public static void ThrowsNotImplementedExceptionForUnrecognizedTopLevelElement()
258+
{
259+
Should.Throw<NotImplementedException>(() => ParseCsproj("""
260+
<Project Sdk="Microsoft.NET.Sdk">
261+
262+
<Xyz></Xyz>
263+
264+
</Project>
265+
"""));
266+
}
267+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
5+
namespace ExampleTester;
6+
7+
public sealed record CsprojParseResult(
8+
string? AssemblyName,
9+
string TargetFramework,
10+
CSharpParseOptions ParseOptions,
11+
CSharpCompilationOptions CompilationOptions,
12+
ImmutableArray<SyntaxTree> GeneratedSources);

tools/ExampleTester/ExampleTester.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="Basic.Reference.Assemblies.Net60" Version="1.8.2" />
1112
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
1213
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
1314
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />

0 commit comments

Comments
 (0)