From aed3a7fd1b198f1f957e820a1c0aed4897d55f5d Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 1 Jun 2025 15:00:10 -0400 Subject: [PATCH 1/3] 25x nonparallelized speedup by avoiding MSBuildWorkspace except for multi-project examples --- tools/ExampleTester/CsprojParseResult.cs | 12 ++ tools/ExampleTester/ExampleTester.csproj | 1 + .../FastCsprojCompilationParser.cs | 145 ++++++++++++++++++ tools/ExampleTester/GeneratedExample.cs | 27 ++-- 4 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 tools/ExampleTester/CsprojParseResult.cs create mode 100644 tools/ExampleTester/FastCsprojCompilationParser.cs diff --git a/tools/ExampleTester/CsprojParseResult.cs b/tools/ExampleTester/CsprojParseResult.cs new file mode 100644 index 000000000..7cddb0dcf --- /dev/null +++ b/tools/ExampleTester/CsprojParseResult.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace ExampleTester; + +internal sealed record CsprojParseResult( + string? AssemblyName, + string TargetFramework, + CSharpParseOptions ParseOptions, + CSharpCompilationOptions CompilationOptions, + ImmutableArray GeneratedSources); diff --git a/tools/ExampleTester/ExampleTester.csproj b/tools/ExampleTester/ExampleTester.csproj index 46f73c0cd..48483697a 100644 --- a/tools/ExampleTester/ExampleTester.csproj +++ b/tools/ExampleTester/ExampleTester.csproj @@ -8,6 +8,7 @@ + diff --git a/tools/ExampleTester/FastCsprojCompilationParser.cs b/tools/ExampleTester/FastCsprojCompilationParser.cs new file mode 100644 index 000000000..59e65114f --- /dev/null +++ b/tools/ExampleTester/FastCsprojCompilationParser.cs @@ -0,0 +1,145 @@ +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Globalization; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace ExampleTester; + +internal static class FastCsprojCompilationParser +{ + public static CSharpCompilation CreateCompilation(string csprojPath) + { + var csproj = ParseCsproj(csprojPath); + + var syntaxTrees = Directory.GetFiles(Path.GetDirectoryName(csprojPath)!, "*.cs").AsParallel().Select(path => + { + using var stream = File.OpenRead(path); + return CSharpSyntaxTree.ParseText(SourceText.From(stream), csproj.ParseOptions, path); + }); + + return CSharpCompilation.Create( + csproj.AssemblyName, + csproj.GeneratedSources.AddRange(syntaxTrees), + SupportedTargetFrameworks[csproj.TargetFramework].References, + csproj.CompilationOptions); + } + + private static readonly FrozenDictionary SupportedOutputKinds = new KeyValuePair[] + { + new("Exe", OutputKind.ConsoleApplication), + new("Library", OutputKind.DynamicallyLinkedLibrary), + new("WinExe", OutputKind.WindowsApplication), + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + private sealed record TargetFrameworkInfo( + ImmutableArray References, + LanguageVersion DefaultLanguageVersion, + int DefaultWarningLevel); + + private static readonly FrozenDictionary SupportedTargetFrameworks = new KeyValuePair[] + { + new("net6.0", new(Basic.Reference.Assemblies.Net60.References.All, LanguageVersion.CSharp10, DefaultWarningLevel: 6)), + }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + + private static CsprojParseResult ParseCsproj(string filePath) + { + var csprojDocument = XDocument.Load(filePath); + + string? assemblyName = null; + string? targetFramework = null; + var parseOptions = CSharpParseOptions.Default; + var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true); + var implicitUsings = false; + + foreach (var element in csprojDocument.Root!.Elements()) + { + switch (element.Name.LocalName) + { + case "PropertyGroup": + foreach (var property in element.Elements()) + { + switch (property.Name.LocalName) + { + case "OutputType": + compilationOptions = compilationOptions.WithOutputKind(SupportedOutputKinds[property.Value]); + break; + case "TargetFramework": + targetFramework = property.Value; + break; + case "Nullable": + compilationOptions = compilationOptions.WithNullableContextOptions( + Enum.Parse(property.Value, ignoreCase: true)); + break; + case "AssemblyName": + assemblyName = property.Value; + break; + case "AllowUnsafeBlocks": + compilationOptions = compilationOptions.WithAllowUnsafe(bool.Parse(property.Value)); + break; + case "LangVersion": + var enumMemberName = property.Value; + + if (decimal.TryParse( + property.Value, + NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowDecimalPoint, + CultureInfo.InvariantCulture, + out var decimalValue)) + { + enumMemberName = "CSharp" + decimalValue.ToString("0.#", CultureInfo.InvariantCulture).Replace('.', '_'); + } + + parseOptions = parseOptions.WithLanguageVersion(Enum.Parse(enumMemberName, ignoreCase: true)); + break; + case "ImplicitUsings": + implicitUsings = ParseFeatureBool(property.Value); + break; + default: + throw new NotImplementedException($"Review whether support is required for <{property.Name}> ({filePath})"); + } + } + break; + + default: + throw new NotImplementedException($"Review whether support is required for <{element.Name}> ({filePath})"); + } + } + + if (parseOptions.SpecifiedLanguageVersion == LanguageVersion.Default) + parseOptions = parseOptions.WithLanguageVersion(SupportedTargetFrameworks[targetFramework!].DefaultLanguageVersion); + + compilationOptions = compilationOptions.WithWarningLevel(SupportedTargetFrameworks[targetFramework!].DefaultWarningLevel); + + var generatedSources = ImmutableArray.CreateBuilder(); + + if (implicitUsings) + { + generatedSources.Add(SyntaxFactory.ParseSyntaxTree(""" + global using System; + global using System.Collections.Generic; + global using System.IO; + global using System.Linq; + global using System.Net.Http; + global using System.Threading; + global using System.Threading.Tasks; + """, + parseOptions, + assemblyName + ".GlobalUsings.g.cs")); + } + + return new CsprojParseResult( + assemblyName, + targetFramework!, + parseOptions, + compilationOptions, + generatedSources.DrainToImmutable()); + } + + private static bool ParseFeatureBool(string propertyValue) + { + return propertyValue.Equals("enable", StringComparison.OrdinalIgnoreCase) + || propertyValue.Equals("true", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/tools/ExampleTester/GeneratedExample.cs b/tools/ExampleTester/GeneratedExample.cs index 6c06eab33..c8b646043 100644 --- a/tools/ExampleTester/GeneratedExample.cs +++ b/tools/ExampleTester/GeneratedExample.cs @@ -1,10 +1,10 @@ -using ExampleExtractor; +using System.Reflection; +using System.Text; +using ExampleExtractor; using Microsoft.Build.Locator; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.MSBuild; using Newtonsoft.Json; -using System.Reflection; -using System.Text; using Utilities; namespace ExampleTester; @@ -38,18 +38,25 @@ internal async Task Test(TesterConfiguration configuration, StatusCheckLog { logger.ConsoleOnlyLog(Metadata.Source, Metadata.StartLine, Metadata.EndLine, $"Testing {Metadata.Name} from {Metadata.Source}", "ExampleTester"); - // Explicitly do a release build, to avoid implicitly defining DEBUG. - var properties = new Dictionary { { "Configuration", "Release" } }; - using var workspace = MSBuildWorkspace.Create(properties); // TODO: Validate this more cleanly. var projectFile = Metadata.Project is string specifiedProject ? Path.Combine(directory, $"{specifiedProject}.csproj") : Directory.GetFiles(directory, "*.csproj").Single(); - var project = await workspace.OpenProjectAsync(projectFile); - var compilation = await project.GetCompilationAsync(); - if (compilation is null) + + Compilation compilation; + try { - throw new InvalidOperationException("Project has no Compilation"); + compilation = FastCsprojCompilationParser.CreateCompilation(projectFile); + } + catch (NotImplementedException) + { + // Explicitly do a release build, to avoid implicitly defining DEBUG. + var properties = new Dictionary { { "Configuration", "Release" } }; + using var workspace = MSBuildWorkspace.Create(properties); + + var project = await workspace.OpenProjectAsync(projectFile); + compilation = await project.GetCompilationAsync() + ?? throw new InvalidOperationException("Project has no Compilation"); } bool ret = true; From fb4e8a54cfd55fcdae1003228dca4fa80b778754 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 1 Jun 2025 22:50:52 -0400 Subject: [PATCH 2/3] Add test coverage for new csproj parser --- .../ExampleTester.Tests.csproj | 25 +++ .../FastCsprojCompilationParserTests.cs | 208 ++++++++++++++++++ tools/ExampleTester/CsprojParseResult.cs | 2 +- .../FastCsprojCompilationParser.cs | 12 +- tools/tools.sln | 14 ++ 5 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 tools/ExampleTester.Tests/ExampleTester.Tests.csproj create mode 100644 tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs diff --git a/tools/ExampleTester.Tests/ExampleTester.Tests.csproj b/tools/ExampleTester.Tests/ExampleTester.Tests.csproj new file mode 100644 index 000000000..350242942 --- /dev/null +++ b/tools/ExampleTester.Tests/ExampleTester.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs b/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs new file mode 100644 index 000000000..f42520383 --- /dev/null +++ b/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs @@ -0,0 +1,208 @@ +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Shouldly; + +namespace ExampleTester.Tests; + +#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive). + +public static class FastCsprojCompilationParserTests +{ + private static CsprojParseResult ParseCsproj(string csprojContents) + { + return FastCsprojCompilationParser.ParseCsproj(XDocument.Parse(csprojContents), "Test.csproj"); + } + + [Test] + public static void Defaults() + { + var result = ParseCsproj(""" + + + + net6.0 + + + + """); + + result.CompilationOptions.OutputKind.ShouldBe(OutputKind.DynamicallyLinkedLibrary); + result.CompilationOptions.NullableContextOptions.ShouldBe(NullableContextOptions.Disable); + result.AssemblyName.ShouldBeNull(); + result.CompilationOptions.AllowUnsafe.ShouldBeFalse(); + result.ParseOptions.LanguageVersion.ShouldBe(LanguageVersion.CSharp10); // Due to net6.0 + result.CompilationOptions.WarningLevel.ShouldBe(6); // Due to net6.0 + result.GeneratedSources.ShouldBeEmpty(); + } + + [Test] + public static void ParsesTargetFramework() + { + ParseCsproj(""" + + + + net6.0 + + + + """).TargetFramework.ShouldBe("net6.0"); + } + + [Test] + public static void ParsesOutputType([Values("Library", "Exe", "WinExe")] string outputType) + { + ParseCsproj($""" + + + + net6.0 + {outputType} + + + + """).CompilationOptions.OutputKind.ShouldBe(outputType switch + { + "Library" => OutputKind.DynamicallyLinkedLibrary, + "Exe" => OutputKind.ConsoleApplication, + "WinExe" => OutputKind.WindowsApplication, + }); + } + + [Test] + public static void ParsesNullable([Values("enable", "disable", "annotations", "warnings")] string nullable) + { + ParseCsproj($""" + + + + net6.0 + {nullable} + + + + """).CompilationOptions.NullableContextOptions.ShouldBe(nullable switch + { + "enable" => NullableContextOptions.Enable, + "disable" => NullableContextOptions.Disable, + "annotations" => NullableContextOptions.Annotations, + "warnings" => NullableContextOptions.Warnings, + }); + } + + [Test] + public static void ParsesAssemblyName() + { + ParseCsproj($""" + + + + net6.0 + Xyz + + + + """).AssemblyName.ShouldBe("Xyz"); + } + + [Test] + public static void ParsesAllowUnsafeBlocks([Values("true", "false")] string allowUnsafeBlocks) + { + ParseCsproj($""" + + + + net6.0 + {allowUnsafeBlocks} + + + + """).CompilationOptions.AllowUnsafe.ShouldBe(allowUnsafeBlocks switch + { + "true" => true, + "false" => false, + }); + } + + [Test] + public static void ParsesImplicitUsings([Values("true", "enable", "false", "disable")] string implicitUsings) + { + var hasImplicitUsings = implicitUsings switch + { + "true" or "enable" => true, + "false" or "disable" => false, + }; + + var result = ParseCsproj($""" + + + + net6.0 + {implicitUsings} + + + + """); + + if (hasImplicitUsings) + { + var source = result.GeneratedSources.ShouldHaveSingleItem(); + source.Options.ShouldBe(result.ParseOptions); + source.FilePath.ShouldBe("Test.GlobalUsings.g.cs"); + source.GetText().ToString().ShouldBe(""" + global using System; + global using System.Collections.Generic; + global using System.IO; + global using System.Linq; + global using System.Net.Http; + global using System.Threading; + global using System.Threading.Tasks; + """); + } + else + { + result.GeneratedSources.ShouldBeEmpty(); + } + } + + [Test] + public static void ThrowsNotImplementedExceptionForUnrecognizedProperty() + { + Should.Throw(() => ParseCsproj(""" + + + + + + + + """)); + } + + [Test] + public static void ThrowsNotImplementedExceptionForUnrecognizedItem() + { + Should.Throw(() => ParseCsproj(""" + + + + + + + + """)); + } + + [Test] + public static void ThrowsNotImplementedExceptionForUnrecognizedTopLevelElement() + { + Should.Throw(() => ParseCsproj(""" + + + + + + """)); + } +} diff --git a/tools/ExampleTester/CsprojParseResult.cs b/tools/ExampleTester/CsprojParseResult.cs index 7cddb0dcf..11c2e8da9 100644 --- a/tools/ExampleTester/CsprojParseResult.cs +++ b/tools/ExampleTester/CsprojParseResult.cs @@ -4,7 +4,7 @@ namespace ExampleTester; -internal sealed record CsprojParseResult( +public sealed record CsprojParseResult( string? AssemblyName, string TargetFramework, CSharpParseOptions ParseOptions, diff --git a/tools/ExampleTester/FastCsprojCompilationParser.cs b/tools/ExampleTester/FastCsprojCompilationParser.cs index 59e65114f..51f1c6ab0 100644 --- a/tools/ExampleTester/FastCsprojCompilationParser.cs +++ b/tools/ExampleTester/FastCsprojCompilationParser.cs @@ -8,11 +8,11 @@ namespace ExampleTester; -internal static class FastCsprojCompilationParser +public static class FastCsprojCompilationParser { public static CSharpCompilation CreateCompilation(string csprojPath) { - var csproj = ParseCsproj(csprojPath); + var csproj = ParseCsproj(XDocument.Load(csprojPath), csprojPath); var syntaxTrees = Directory.GetFiles(Path.GetDirectoryName(csprojPath)!, "*.cs").AsParallel().Select(path => { @@ -44,10 +44,8 @@ private sealed record TargetFrameworkInfo( new("net6.0", new(Basic.Reference.Assemblies.Net60.References.All, LanguageVersion.CSharp10, DefaultWarningLevel: 6)), }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); - private static CsprojParseResult ParseCsproj(string filePath) + public static CsprojParseResult ParseCsproj(XDocument csprojDocument, string filePath) { - var csprojDocument = XDocument.Load(filePath); - string? assemblyName = null; string? targetFramework = null; var parseOptions = CSharpParseOptions.Default; @@ -116,6 +114,8 @@ private static CsprojParseResult ParseCsproj(string filePath) if (implicitUsings) { + var projectName = Path.GetFileNameWithoutExtension(filePath); + generatedSources.Add(SyntaxFactory.ParseSyntaxTree(""" global using System; global using System.Collections.Generic; @@ -126,7 +126,7 @@ private static CsprojParseResult ParseCsproj(string filePath) global using System.Threading.Tasks; """, parseOptions, - assemblyName + ".GlobalUsings.g.cs")); + projectName + ".GlobalUsings.g.cs")); } return new CsprojParseResult( diff --git a/tools/tools.sln b/tools/tools.sln index be1786ffb..89bc77573 100644 --- a/tools/tools.sln +++ b/tools/tools.sln @@ -20,6 +20,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleTester", "ExampleTes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleFormatter", "ExampleFormatter\ExampleFormatter.csproj", "{82D1A159-5637-48C4-845D-CC1390995CC2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleTester.Tests", "ExampleTester.Tests\ExampleTester.Tests.csproj", "{6AE7A04F-85DE-46EA-8696-F748B6130971}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +116,18 @@ Global {82D1A159-5637-48C4-845D-CC1390995CC2}.Release|x64.Build.0 = Release|Any CPU {82D1A159-5637-48C4-845D-CC1390995CC2}.Release|x86.ActiveCfg = Release|Any CPU {82D1A159-5637-48C4-845D-CC1390995CC2}.Release|x86.Build.0 = Release|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x64.ActiveCfg = Debug|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x64.Build.0 = Debug|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x86.ActiveCfg = Debug|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Debug|x86.Build.0 = Debug|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|Any CPU.Build.0 = Release|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x64.ActiveCfg = Release|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x64.Build.0 = Release|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x86.ActiveCfg = Release|Any CPU + {6AE7A04F-85DE-46EA-8696-F748B6130971}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b28f1af23257f82d77122234b7f5ca428a154966 Mon Sep 17 00:00:00 2001 From: jnm2 Date: Sun, 1 Jun 2025 23:54:05 -0400 Subject: [PATCH 3/3] Require all csproj parsing tests to match MSBuild --- tools/ExampleTester.Tests/AssemblyInfo.cs | 1 + .../FastCsprojCompilationParserTests.cs | 79 ++++++++++++++++--- .../FastCsprojCompilationParser.cs | 46 +++++++---- 3 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 tools/ExampleTester.Tests/AssemblyInfo.cs diff --git a/tools/ExampleTester.Tests/AssemblyInfo.cs b/tools/ExampleTester.Tests/AssemblyInfo.cs new file mode 100644 index 000000000..72eb61577 --- /dev/null +++ b/tools/ExampleTester.Tests/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: Parallelizable(ParallelScope.Children)] diff --git a/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs b/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs index f42520383..c07faaf74 100644 --- a/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs +++ b/tools/ExampleTester.Tests/FastCsprojCompilationParserTests.cs @@ -1,6 +1,8 @@ -using System.Xml.Linq; +using System.Collections.Immutable; +using System.Xml.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.MSBuild; using Shouldly; namespace ExampleTester.Tests; @@ -9,9 +11,64 @@ namespace ExampleTester.Tests; public static class FastCsprojCompilationParserTests { + private const string CsprojFileName = "Test.csproj"; + private static CsprojParseResult ParseCsproj(string csprojContents) { - return FastCsprojCompilationParser.ParseCsproj(XDocument.Parse(csprojContents), "Test.csproj"); + var result = FastCsprojCompilationParser.ParseCsproj(XDocument.Parse(csprojContents), CsprojFileName); + CompareMSBuildWorkspaceCompilation(csprojContents, result); + return result; + } + + private static void CompareMSBuildWorkspaceCompilation(string csprojContents, CsprojParseResult result) + { + var msbuildCompilation = GetMSBuildWorkspaceCompilation(csprojContents); + + result.AssemblyName.ShouldBe(msbuildCompilation.AssemblyName); + + var sanitizedCompilationOptions = msbuildCompilation.Options + .WithAssemblyIdentityComparer(result.CompilationOptions.AssemblyIdentityComparer) + .WithMetadataReferenceResolver(null) + .WithSourceReferenceResolver(null) + .WithStrongNameProvider(null) + .WithSyntaxTreeOptionsProvider(null) + .WithXmlReferenceResolver(null); + + result.CompilationOptions.Equals(sanitizedCompilationOptions).ShouldBeTrue(); + + var sanitizedParseOptions = msbuildCompilation.SyntaxTrees.First().Options; + + result.ParseOptions.Equals(sanitizedParseOptions).ShouldBeTrue(); + + var sanitizedSyntaxTrees = msbuildCompilation.SyntaxTrees + .Where(tree => !new[] { ".AssemblyAttributes.cs", ".AssemblyInfo.cs" }.Any(ending => + Path.GetFileName(tree.FilePath).EndsWith(ending, StringComparison.OrdinalIgnoreCase))) + .ToImmutableArray(); + + result.GeneratedSources.SequenceEqual(sanitizedSyntaxTrees, (a, b) => + Path.GetFileName(a.FilePath).Equals(Path.GetFileName(b.FilePath), StringComparison.OrdinalIgnoreCase) + && a.Options.Equals(b.Options) + && a.GetText().ToString() == b.GetText().ToString()) + .ShouldBeTrue(); + } + + private static Compilation GetMSBuildWorkspaceCompilation(string csprojContents) + { + using var workspace = MSBuildWorkspace.Create(new Dictionary { ["Configuration"] = "Release" }); + + var tempFolder = Path.Join(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempFolder); + try + { + var csprojPath = Path.Join(tempFolder, CsprojFileName); + File.WriteAllText(csprojPath, csprojContents); + var project = workspace.OpenProjectAsync(csprojPath).GetAwaiter().GetResult(); + return project.GetCompilationAsync().GetAwaiter().GetResult().ShouldNotBeNull(); + } + finally + { + Directory.Delete(tempFolder, recursive: true); + } } [Test] @@ -29,7 +86,7 @@ public static void Defaults() result.CompilationOptions.OutputKind.ShouldBe(OutputKind.DynamicallyLinkedLibrary); result.CompilationOptions.NullableContextOptions.ShouldBe(NullableContextOptions.Disable); - result.AssemblyName.ShouldBeNull(); + result.AssemblyName.ShouldBe(Path.GetFileNameWithoutExtension(CsprojFileName)); result.CompilationOptions.AllowUnsafe.ShouldBeFalse(); result.ParseOptions.LanguageVersion.ShouldBe(LanguageVersion.CSharp10); // Due to net6.0 result.CompilationOptions.WarningLevel.ShouldBe(6); // Due to net6.0 @@ -151,13 +208,15 @@ public static void ParsesImplicitUsings([Values("true", "enable", "false", "disa source.Options.ShouldBe(result.ParseOptions); source.FilePath.ShouldBe("Test.GlobalUsings.g.cs"); source.GetText().ToString().ShouldBe(""" - global using System; - global using System.Collections.Generic; - global using System.IO; - global using System.Linq; - global using System.Net.Http; - global using System.Threading; - global using System.Threading.Tasks; + // + global using global::System; + global using global::System.Collections.Generic; + global using global::System.IO; + global using global::System.Linq; + global using global::System.Net.Http; + global using global::System.Threading; + global using global::System.Threading.Tasks; + """); } else diff --git a/tools/ExampleTester/FastCsprojCompilationParser.cs b/tools/ExampleTester/FastCsprojCompilationParser.cs index 51f1c6ab0..51c7f1098 100644 --- a/tools/ExampleTester/FastCsprojCompilationParser.cs +++ b/tools/ExampleTester/FastCsprojCompilationParser.cs @@ -44,12 +44,28 @@ private sealed record TargetFrameworkInfo( new("net6.0", new(Basic.Reference.Assemblies.Net60.References.All, LanguageVersion.CSharp10, DefaultWarningLevel: 6)), }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); + private static readonly CSharpParseOptions DefaultParseOptions = new(preprocessorSymbols: [ + "TRACE", "RELEASE", "NET", "NET6_0", "NETCOREAPP", "NET5_0_OR_GREATER", "NET6_0_OR_GREATER", + "NETCOREAPP1_0_OR_GREATER", "NETCOREAPP1_1_OR_GREATER", "NETCOREAPP2_0_OR_GREATER", + "NETCOREAPP2_1_OR_GREATER", "NETCOREAPP2_2_OR_GREATER", "NETCOREAPP3_0_OR_GREATER", + "NETCOREAPP3_1_OR_GREATER"]); + + private static readonly CSharpCompilationOptions DefaultCompilationOptions = new( + OutputKind.DynamicallyLinkedLibrary, + deterministic: true, + optimizationLevel: OptimizationLevel.Release, + specificDiagnosticOptions: [ + new("NU1605", ReportDiagnostic.Error), + new("CS1702", ReportDiagnostic.Suppress), + new("CS1701", ReportDiagnostic.Suppress)]); + public static CsprojParseResult ParseCsproj(XDocument csprojDocument, string filePath) { - string? assemblyName = null; - string? targetFramework = null; - var parseOptions = CSharpParseOptions.Default; - var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, deterministic: true); + var projectName = Path.GetFileNameWithoutExtension(filePath); + var assemblyName = projectName; + var targetFramework = (string?)null; + var parseOptions = DefaultParseOptions; + var compilationOptions = DefaultCompilationOptions; var implicitUsings = false; foreach (var element in csprojDocument.Root!.Elements()) @@ -108,22 +124,24 @@ public static CsprojParseResult ParseCsproj(XDocument csprojDocument, string fil if (parseOptions.SpecifiedLanguageVersion == LanguageVersion.Default) parseOptions = parseOptions.WithLanguageVersion(SupportedTargetFrameworks[targetFramework!].DefaultLanguageVersion); - compilationOptions = compilationOptions.WithWarningLevel(SupportedTargetFrameworks[targetFramework!].DefaultWarningLevel); + compilationOptions = compilationOptions + .WithWarningLevel(SupportedTargetFrameworks[targetFramework!].DefaultWarningLevel) + .WithModuleName(assemblyName + ".dll"); var generatedSources = ImmutableArray.CreateBuilder(); if (implicitUsings) { - var projectName = Path.GetFileNameWithoutExtension(filePath); - generatedSources.Add(SyntaxFactory.ParseSyntaxTree(""" - global using System; - global using System.Collections.Generic; - global using System.IO; - global using System.Linq; - global using System.Net.Http; - global using System.Threading; - global using System.Threading.Tasks; + // + global using global::System; + global using global::System.Collections.Generic; + global using global::System.IO; + global using global::System.Linq; + global using global::System.Net.Http; + global using global::System.Threading; + global using global::System.Threading.Tasks; + """, parseOptions, projectName + ".GlobalUsings.g.cs"));