Skip to content

Commit fb4e8a5

Browse files
committed
Add test coverage for new csproj parser
1 parent aed3a7f commit fb4e8a5

File tree

5 files changed

+254
-7
lines changed

5 files changed

+254
-7
lines changed
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: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
using System.Xml.Linq;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Shouldly;
5+
6+
namespace ExampleTester.Tests;
7+
8+
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
9+
10+
public static class FastCsprojCompilationParserTests
11+
{
12+
private static CsprojParseResult ParseCsproj(string csprojContents)
13+
{
14+
return FastCsprojCompilationParser.ParseCsproj(XDocument.Parse(csprojContents), "Test.csproj");
15+
}
16+
17+
[Test]
18+
public static void Defaults()
19+
{
20+
var result = ParseCsproj("""
21+
<Project Sdk="Microsoft.NET.Sdk">
22+
23+
<PropertyGroup>
24+
<TargetFramework>net6.0</TargetFramework>
25+
</PropertyGroup>
26+
27+
</Project>
28+
""");
29+
30+
result.CompilationOptions.OutputKind.ShouldBe(OutputKind.DynamicallyLinkedLibrary);
31+
result.CompilationOptions.NullableContextOptions.ShouldBe(NullableContextOptions.Disable);
32+
result.AssemblyName.ShouldBeNull();
33+
result.CompilationOptions.AllowUnsafe.ShouldBeFalse();
34+
result.ParseOptions.LanguageVersion.ShouldBe(LanguageVersion.CSharp10); // Due to net6.0
35+
result.CompilationOptions.WarningLevel.ShouldBe(6); // Due to net6.0
36+
result.GeneratedSources.ShouldBeEmpty();
37+
}
38+
39+
[Test]
40+
public static void ParsesTargetFramework()
41+
{
42+
ParseCsproj("""
43+
<Project Sdk="Microsoft.NET.Sdk">
44+
45+
<PropertyGroup>
46+
<TargetFramework>net6.0</TargetFramework>
47+
</PropertyGroup>
48+
49+
</Project>
50+
""").TargetFramework.ShouldBe("net6.0");
51+
}
52+
53+
[Test]
54+
public static void ParsesOutputType([Values("Library", "Exe", "WinExe")] string outputType)
55+
{
56+
ParseCsproj($"""
57+
<Project Sdk="Microsoft.NET.Sdk">
58+
59+
<PropertyGroup>
60+
<TargetFramework>net6.0</TargetFramework>
61+
<OutputType>{outputType}</OutputType>
62+
</PropertyGroup>
63+
64+
</Project>
65+
""").CompilationOptions.OutputKind.ShouldBe(outputType switch
66+
{
67+
"Library" => OutputKind.DynamicallyLinkedLibrary,
68+
"Exe" => OutputKind.ConsoleApplication,
69+
"WinExe" => OutputKind.WindowsApplication,
70+
});
71+
}
72+
73+
[Test]
74+
public static void ParsesNullable([Values("enable", "disable", "annotations", "warnings")] string nullable)
75+
{
76+
ParseCsproj($"""
77+
<Project Sdk="Microsoft.NET.Sdk">
78+
79+
<PropertyGroup>
80+
<TargetFramework>net6.0</TargetFramework>
81+
<Nullable>{nullable}</Nullable>
82+
</PropertyGroup>
83+
84+
</Project>
85+
""").CompilationOptions.NullableContextOptions.ShouldBe(nullable switch
86+
{
87+
"enable" => NullableContextOptions.Enable,
88+
"disable" => NullableContextOptions.Disable,
89+
"annotations" => NullableContextOptions.Annotations,
90+
"warnings" => NullableContextOptions.Warnings,
91+
});
92+
}
93+
94+
[Test]
95+
public static void ParsesAssemblyName()
96+
{
97+
ParseCsproj($"""
98+
<Project Sdk="Microsoft.NET.Sdk">
99+
100+
<PropertyGroup>
101+
<TargetFramework>net6.0</TargetFramework>
102+
<AssemblyName>Xyz</AssemblyName>
103+
</PropertyGroup>
104+
105+
</Project>
106+
""").AssemblyName.ShouldBe("Xyz");
107+
}
108+
109+
[Test]
110+
public static void ParsesAllowUnsafeBlocks([Values("true", "false")] string allowUnsafeBlocks)
111+
{
112+
ParseCsproj($"""
113+
<Project Sdk="Microsoft.NET.Sdk">
114+
115+
<PropertyGroup>
116+
<TargetFramework>net6.0</TargetFramework>
117+
<AllowUnsafeBlocks>{allowUnsafeBlocks}</AllowUnsafeBlocks>
118+
</PropertyGroup>
119+
120+
</Project>
121+
""").CompilationOptions.AllowUnsafe.ShouldBe(allowUnsafeBlocks switch
122+
{
123+
"true" => true,
124+
"false" => false,
125+
});
126+
}
127+
128+
[Test]
129+
public static void ParsesImplicitUsings([Values("true", "enable", "false", "disable")] string implicitUsings)
130+
{
131+
var hasImplicitUsings = implicitUsings switch
132+
{
133+
"true" or "enable" => true,
134+
"false" or "disable" => false,
135+
};
136+
137+
var result = ParseCsproj($"""
138+
<Project Sdk="Microsoft.NET.Sdk">
139+
140+
<PropertyGroup>
141+
<TargetFramework>net6.0</TargetFramework>
142+
<ImplicitUsings>{implicitUsings}</ImplicitUsings>
143+
</PropertyGroup>
144+
145+
</Project>
146+
""");
147+
148+
if (hasImplicitUsings)
149+
{
150+
var source = result.GeneratedSources.ShouldHaveSingleItem();
151+
source.Options.ShouldBe(result.ParseOptions);
152+
source.FilePath.ShouldBe("Test.GlobalUsings.g.cs");
153+
source.GetText().ToString().ShouldBe("""
154+
global using System;
155+
global using System.Collections.Generic;
156+
global using System.IO;
157+
global using System.Linq;
158+
global using System.Net.Http;
159+
global using System.Threading;
160+
global using System.Threading.Tasks;
161+
""");
162+
}
163+
else
164+
{
165+
result.GeneratedSources.ShouldBeEmpty();
166+
}
167+
}
168+
169+
[Test]
170+
public static void ThrowsNotImplementedExceptionForUnrecognizedProperty()
171+
{
172+
Should.Throw<NotImplementedException>(() => ParseCsproj("""
173+
<Project Sdk="Microsoft.NET.Sdk">
174+
175+
<PropertyGroup>
176+
<Xyz></Xyz>
177+
</PropertyGroup>
178+
179+
</Project>
180+
"""));
181+
}
182+
183+
[Test]
184+
public static void ThrowsNotImplementedExceptionForUnrecognizedItem()
185+
{
186+
Should.Throw<NotImplementedException>(() => ParseCsproj("""
187+
<Project Sdk="Microsoft.NET.Sdk">
188+
189+
<ItemGroup>
190+
<Xyz Include="" />
191+
</ItemGroup>
192+
193+
</Project>
194+
"""));
195+
}
196+
197+
[Test]
198+
public static void ThrowsNotImplementedExceptionForUnrecognizedTopLevelElement()
199+
{
200+
Should.Throw<NotImplementedException>(() => ParseCsproj("""
201+
<Project Sdk="Microsoft.NET.Sdk">
202+
203+
<Xyz></Xyz>
204+
205+
</Project>
206+
"""));
207+
}
208+
}

tools/ExampleTester/CsprojParseResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace ExampleTester;
66

7-
internal sealed record CsprojParseResult(
7+
public sealed record CsprojParseResult(
88
string? AssemblyName,
99
string TargetFramework,
1010
CSharpParseOptions ParseOptions,

tools/ExampleTester/FastCsprojCompilationParser.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
namespace ExampleTester;
1010

11-
internal static class FastCsprojCompilationParser
11+
public static class FastCsprojCompilationParser
1212
{
1313
public static CSharpCompilation CreateCompilation(string csprojPath)
1414
{
15-
var csproj = ParseCsproj(csprojPath);
15+
var csproj = ParseCsproj(XDocument.Load(csprojPath), csprojPath);
1616

1717
var syntaxTrees = Directory.GetFiles(Path.GetDirectoryName(csprojPath)!, "*.cs").AsParallel().Select(path =>
1818
{
@@ -44,10 +44,8 @@ private sealed record TargetFrameworkInfo(
4444
new("net6.0", new(Basic.Reference.Assemblies.Net60.References.All, LanguageVersion.CSharp10, DefaultWarningLevel: 6)),
4545
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
4646

47-
private static CsprojParseResult ParseCsproj(string filePath)
47+
public static CsprojParseResult ParseCsproj(XDocument csprojDocument, string filePath)
4848
{
49-
var csprojDocument = XDocument.Load(filePath);
50-
5149
string? assemblyName = null;
5250
string? targetFramework = null;
5351
var parseOptions = CSharpParseOptions.Default;
@@ -116,6 +114,8 @@ private static CsprojParseResult ParseCsproj(string filePath)
116114

117115
if (implicitUsings)
118116
{
117+
var projectName = Path.GetFileNameWithoutExtension(filePath);
118+
119119
generatedSources.Add(SyntaxFactory.ParseSyntaxTree("""
120120
global using System;
121121
global using System.Collections.Generic;
@@ -126,7 +126,7 @@ private static CsprojParseResult ParseCsproj(string filePath)
126126
global using System.Threading.Tasks;
127127
""",
128128
parseOptions,
129-
assemblyName + ".GlobalUsings.g.cs"));
129+
projectName + ".GlobalUsings.g.cs"));
130130
}
131131

132132
return new CsprojParseResult(

tools/tools.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleTester", "ExampleTes
2020
EndProject
2121
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleFormatter", "ExampleFormatter\ExampleFormatter.csproj", "{82D1A159-5637-48C4-845D-CC1390995CC2}"
2222
EndProject
23+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleTester.Tests", "ExampleTester.Tests\ExampleTester.Tests.csproj", "{6AE7A04F-85DE-46EA-8696-F748B6130971}"
24+
EndProject
2325
Global
2426
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2527
Debug|Any CPU = Debug|Any CPU
@@ -114,6 +116,18 @@ Global
114116
{82D1A159-5637-48C4-845D-CC1390995CC2}.Release|x64.Build.0 = Release|Any CPU
115117
{82D1A159-5637-48C4-845D-CC1390995CC2}.Release|x86.ActiveCfg = Release|Any CPU
116118
{82D1A159-5637-48C4-845D-CC1390995CC2}.Release|x86.Build.0 = Release|Any CPU
119+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
120+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|Any CPU.Build.0 = Debug|Any CPU
121+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x64.ActiveCfg = Debug|Any CPU
122+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x64.Build.0 = Debug|Any CPU
123+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x86.ActiveCfg = Debug|Any CPU
124+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x86.Build.0 = Debug|Any CPU
125+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|Any CPU.ActiveCfg = Release|Any CPU
126+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|Any CPU.Build.0 = Release|Any CPU
127+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x64.ActiveCfg = Release|Any CPU
128+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x64.Build.0 = Release|Any CPU
129+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x86.ActiveCfg = Release|Any CPU
130+
{6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x86.Build.0 = Release|Any CPU
117131
EndGlobalSection
118132
GlobalSection(SolutionProperties) = preSolution
119133
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)