diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs index d6917d3..21c9dee 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs @@ -8,6 +8,8 @@ using System.Threading.Tasks; using System.Web; using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; namespace Basic.CompilerLog.UnitTests; @@ -22,6 +24,8 @@ public sealed class CompilerLogFixture : IDisposable internal string ConsoleComplogPath { get; } + internal string ConsoleNoGeneratorComplogPath { get; } + internal string ClassLibComplogPath { get; } internal string ClassLibSignedComplogPath { get; } @@ -35,16 +39,31 @@ public sealed class CompilerLogFixture : IDisposable internal IEnumerable AllComplogs { get; } - public CompilerLogFixture() + /// + /// Constructor for the primary fixture. To get actual diagnostic messages into the output + /// Add the following to xunit.runner.json to enable "diagnosticMessages": true + /// + public CompilerLogFixture(IMessageSink messageSink) { StorageDirectory = Path.Combine(Path.GetTempPath(), nameof(CompilerLogFixture), Guid.NewGuid().ToString("N")); ComplogDirectory = Path.Combine(StorageDirectory, "logs"); Directory.CreateDirectory(ComplogDirectory); + var diagnosticBuilder = new StringBuilder(); + void RunDotnetCommand(string args, string workingDirectory) + { + diagnosticBuilder.AppendLine($"Running: {args} in {workingDirectory}"); + var result = DotnetUtil.Command(args, workingDirectory); + diagnosticBuilder.AppendLine($"Succeeded: {result.Succeeded}"); + diagnosticBuilder.AppendLine($"Standard Output: {result.StandardOut}"); + diagnosticBuilder.AppendLine($"Standard Error: {result.StandardError}"); + Assert.True(result.Succeeded); + } + var allCompLogs = new List(); - ConsoleComplogPath = WithBuild("console.complog", static string (string scratchPath) => + ConsoleComplogPath = WithBuild("console.complog", string (string scratchPath) => { - DotnetUtil.CommandOrThrow($"new console --name example --output .", scratchPath); + RunDotnetCommand($"new console --name console --output .", scratchPath); var projectFileContent = """ @@ -55,7 +74,7 @@ public CompilerLogFixture() """; - File.WriteAllText(Path.Combine(scratchPath, "example.csproj"), projectFileContent, TestBase.DefaultEncoding); + File.WriteAllText(Path.Combine(scratchPath, "console.csproj"), projectFileContent, TestBase.DefaultEncoding); var program = """ using System; using System.Text.RegularExpressions; @@ -69,13 +88,20 @@ partial class Util { } """; File.WriteAllText(Path.Combine(scratchPath, "Program.cs"), program, TestBase.DefaultEncoding); - Assert.True(DotnetUtil.Command("build -bl", scratchPath).Succeeded); + RunDotnetCommand("build -bl", scratchPath); + return Path.Combine(scratchPath, "msbuild.binlog"); + }); + + ConsoleNoGeneratorComplogPath = WithBuild("console-no-generator.complog", string (string scratchPath) => + { + RunDotnetCommand($"new console --name example-no-generator --output .", scratchPath); + RunDotnetCommand("build -bl", scratchPath); return Path.Combine(scratchPath, "msbuild.binlog"); }); - ClassLibComplogPath = WithBuild("classlib.complog", static string (string scratchPath) => + ClassLibComplogPath = WithBuild("classlib.complog", string (string scratchPath) => { - DotnetUtil.CommandOrThrow($"new classlib --name example --output .", scratchPath); + RunDotnetCommand($"new classlib --name classlib --output .", scratchPath); var projectFileContent = """ @@ -85,7 +111,7 @@ partial class Util { """; - File.WriteAllText(Path.Combine(scratchPath, "example.csproj"), projectFileContent, TestBase.DefaultEncoding); + File.WriteAllText(Path.Combine(scratchPath, "classlib.csproj"), projectFileContent, TestBase.DefaultEncoding); var program = """ using System; using System.Text.RegularExpressions; @@ -96,13 +122,13 @@ partial class Util { } """; File.WriteAllText(Path.Combine(scratchPath, "Class1.cs"), program, TestBase.DefaultEncoding); - Assert.True(DotnetUtil.Command("build -bl", scratchPath).Succeeded); + RunDotnetCommand("build -bl", scratchPath); return Path.Combine(scratchPath, "msbuild.binlog"); }); - ClassLibSignedComplogPath = WithBuild("classlibsigned.complog", static string (string scratchPath) => + ClassLibSignedComplogPath = WithBuild("classlibsigned.complog", string (string scratchPath) => { - DotnetUtil.CommandOrThrow($"new classlib --name example --output .", scratchPath); + RunDotnetCommand($"new classlib --name classlibsigned --output .", scratchPath); var keyFilePath = Path.Combine(scratchPath, "Key.snk"); var projectFileContent = $""" @@ -114,7 +140,7 @@ partial class Util { """; - File.WriteAllText(Path.Combine(scratchPath, "example.csproj"), projectFileContent, TestBase.DefaultEncoding); + File.WriteAllText(Path.Combine(scratchPath, "classlibsigned.csproj"), projectFileContent, TestBase.DefaultEncoding); File.WriteAllBytes(keyFilePath, ResourceLoader.GetResourceBlob("Key.snk")); var program = """ using System; @@ -126,13 +152,13 @@ partial class Util { } """; File.WriteAllText(Path.Combine(scratchPath, "Class1.cs"), program, TestBase.DefaultEncoding); - Assert.True(DotnetUtil.Command("build -bl", scratchPath).Succeeded); + RunDotnetCommand("build -bl", scratchPath); return Path.Combine(scratchPath, "msbuild.binlog"); }); - ClassLibMultiComplogPath = WithBuild("classlibmulti.complog", static string (string scratchPath) => + ClassLibMultiComplogPath = WithBuild("classlibmulti.complog", string (string scratchPath) => { - DotnetUtil.CommandOrThrow($"new classlib --name example --output .", scratchPath); + RunDotnetCommand($"new classlib --name classlibmulti --output .", scratchPath); var projectFileContent = """ @@ -142,7 +168,7 @@ partial class Util { """; - File.WriteAllText(Path.Combine(scratchPath, "example.csproj"), projectFileContent, TestBase.DefaultEncoding); + File.WriteAllText(Path.Combine(scratchPath, "classlibmulti.csproj"), projectFileContent, TestBase.DefaultEncoding); var program = """ using System; using System.Text.RegularExpressions; @@ -152,16 +178,16 @@ partial class Util { } """; File.WriteAllText(Path.Combine(scratchPath, "Class 1.cs"), program, TestBase.DefaultEncoding); - Assert.True(DotnetUtil.Command("build -bl", scratchPath).Succeeded); + RunDotnetCommand("build -bl", scratchPath); return Path.Combine(scratchPath, "msbuild.binlog"); }); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - WpfAppComplogPath = WithBuild("wpfapp.complog", static string (string scratchPath) => + WpfAppComplogPath = WithBuild("wpfapp.complog", string (string scratchPath) => { - Assert.True(DotnetUtil.Command("new wpf --name example --output .", scratchPath).Succeeded); - Assert.True(DotnetUtil.Command("build -bl", scratchPath).Succeeded); + RunDotnetCommand("new wpf --name wpfapp --output .", scratchPath); + RunDotnetCommand("build -bl", scratchPath); return Path.Combine(scratchPath, "msbuild.binlog"); }); } @@ -169,15 +195,25 @@ partial class Util { AllComplogs = allCompLogs; string WithBuild(string name, Func action) { - var scratchPath = Path.Combine(StorageDirectory, "scratch dir"); - Directory.CreateDirectory(scratchPath); - var binlogFilePath = action(scratchPath); - var complogFilePath = Path.Combine(ComplogDirectory, name); - var diagnostics = CompilerLogUtil.ConvertBinaryLog(binlogFilePath, complogFilePath); - Assert.Empty(diagnostics); - Directory.Delete(scratchPath, recursive: true); - allCompLogs.Add(complogFilePath); - return complogFilePath; + try + { + var scratchPath = Path.Combine(StorageDirectory, "scratch dir", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(scratchPath); + RunDotnetCommand("new globaljson --sdk-version 7.0.400", scratchPath); + var binlogFilePath = action(scratchPath); + Assert.True(File.Exists(binlogFilePath)); + var complogFilePath = Path.Combine(ComplogDirectory, name); + var diagnostics = CompilerLogUtil.ConvertBinaryLog(binlogFilePath, complogFilePath); + Assert.Empty(diagnostics); + Directory.Delete(scratchPath, recursive: true); + allCompLogs.Add(complogFilePath); + return complogFilePath; + } + catch (Exception ex) + { + messageSink.OnMessage(new DiagnosticMessage(diagnosticBuilder.ToString())); + throw new Exception($"Cannot generate compiler log {name}", ex); + } } } diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs index da8e0cf..6120931 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs @@ -1,9 +1,11 @@ using Basic.CompilerLog.Util; +using Basic.CompilerLog.Util.Impl; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.ComponentModel.Design.Serialization; +using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -180,10 +182,10 @@ public void AnalyzerLoadCaching(BasicAnalyzerKind kind) var options = new BasicAnalyzerHostOptions(kind, cacheable: true); using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, options: options); - var key = reader.ReadRawCompilationData(0).Item2.Analyzers; + var data = reader.ReadRawCompilationData(0).Item2; - var host1 = reader.ReadAnalyzers(key); - var host2 = reader.ReadAnalyzers(key); + var host1 = reader.ReadAnalyzers(data); + var host2 = reader.ReadAnalyzers(data); Assert.Same(host1, host2); host1.Dispose(); Assert.True(host1.IsDisposed); @@ -263,7 +265,7 @@ public void EmitToMemory() } [Fact] - public void NoAnalyzersGeneratedFilesInRaw() + public void NoneHostGeneratedFilesInRaw() { using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); var (_, data) = reader.ReadRawCompilationData(0); @@ -271,11 +273,11 @@ public void NoAnalyzersGeneratedFilesInRaw() } [Fact] - public void NoAnalyzerGeneratedFilesShouldBeFirst() + public void NoneHostGeneratedFilesShouldBeLast() { using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); var data = reader.ReadCompilationData(0); - var tree = data.Compilation.SyntaxTrees.First(); + var tree = data.GetCompilationAfterGenerators().SyntaxTrees.Last(); var decls = tree.GetRoot().DescendantNodes().OfType().ToList(); Assert.True(decls.Count >= 2); Assert.Equal("Util", decls[0].Identifier.Text); @@ -283,16 +285,59 @@ public void NoAnalyzerGeneratedFilesShouldBeFirst() } [Fact] - public void NoAnalyzerShouldHaveNoAnalyzers() + public void NoneHostAddsFakeGeneratorForGeneratedSource() { using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); var data = reader.ReadCompilationData(0); var compilation1 = data.Compilation; var compilation2 = data.GetCompilationAfterGenerators(); + Assert.NotSame(compilation1, compilation2); + Assert.Single(data.AnalyzerReferences); + } + + [Fact] + public void NoneHostAddsNoGeneratorIfNoGeneratedSource() + { + using var reader = CompilerLogReader.Create(Fixture.ConsoleNoGeneratorComplogPath, BasicAnalyzerHostOptions.None); + var data = reader.ReadCompilationData(0); + var compilation1 = data.Compilation; + var compilation2 = data.GetCompilationAfterGenerators(); Assert.Same(compilation1, compilation2); Assert.Empty(data.AnalyzerReferences); } + [Fact] + public void NoneHostNativePdb() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + RunDotNet($"new console --name example --output ."); + var projectFileContent = """ + + + Exe + Full + net7.0 + enable + enable + + + """; + File.WriteAllText(Path.Combine(RootDirectory, "example.csproj"), projectFileContent, DefaultEncoding); + RunDotNet("build -bl"); + + using var reader = CompilerLogReader.Create(Path.Combine(RootDirectory, "msbuild.binlog"), BasicAnalyzerHostOptions.None); + var rawData = reader.ReadRawCompilationData(0).Item2; + Assert.False(rawData.ReadGeneratedFiles); + var data = reader.ReadCompilationData(0); + var compilation = data.GetCompilationAfterGenerators(out var diagnostics); + Assert.Single(diagnostics); + Assert.Equal(BasicAnalyzerHostNone.CannotReadGeneratedFiles.Id, diagnostics[0].Id); + } + [Fact] public void KindWpf() { diff --git a/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs b/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs index 3d80e92..8190789 100644 --- a/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs +++ b/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs @@ -194,8 +194,8 @@ class C { } [Fact] public void ConsoleWithRuleset() { - RunDotNet($"new console --name example --output ."); - File.WriteAllText(Path.Combine(RootDirectory, "example.csproj"), + RunDotNet($"new console --name console-with-ruleset --output ."); + File.WriteAllText(Path.Combine(RootDirectory, "console-with-ruleset.csproj"), """ diff --git a/src/Basic.CompilerLog.UnitTests/ProgramTests.cs b/src/Basic.CompilerLog.UnitTests/ProgramTests.cs index 8788f9b..d052ae6 100644 --- a/src/Basic.CompilerLog.UnitTests/ProgramTests.cs +++ b/src/Basic.CompilerLog.UnitTests/ProgramTests.cs @@ -62,9 +62,9 @@ public void CreateFullPath() [Fact] public void References() { - Assert.Equal(0, RunCompLog($"ref -o {RootDirectory} {Fixture.ComplogDirectory}")); - Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "example", "refs"), "*.dll")); - Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "example", "analyzers"), "*.dll", SearchOption.AllDirectories)); + Assert.Equal(0, RunCompLog($"ref -o {RootDirectory} {Path.Combine(Fixture.ComplogDirectory, "console.complog")}")); + Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "console", "refs"), "*.dll")); + Assert.NotEmpty(Directory.EnumerateFiles(Path.Combine(RootDirectory, "console", "analyzers"), "*.dll", SearchOption.AllDirectories)); } [Theory] @@ -84,15 +84,17 @@ public void ExportHelloWorld(string template) Assert.True(buildResult.Succeeded); } - [Fact] - public void EmitConsole() + [Theory] + [InlineData("")] + [InlineData("-none")] + public void EmitConsole(string arg) { using var emitDir = new TempDir(); - RunCompLog($"emit -o {emitDir.DirectoryPath} {Fixture.ConsoleComplogPath}"); + RunCompLog($"emit {arg} -o {emitDir.DirectoryPath} {Fixture.ConsoleComplogPath}"); - AssertOutput(@"example\emit\example.dll"); - AssertOutput(@"example\emit\example.pdb"); - AssertOutput(@"example\emit\ref\example.dll"); + AssertOutput(@"console\emit\console.dll"); + AssertOutput(@"console\emit\console.pdb"); + AssertOutput(@"console\emit\ref\console.dll"); void AssertOutput(string relativePath) { diff --git a/src/Basic.CompilerLog.UnitTests/ResilientDirectoryTests.cs b/src/Basic.CompilerLog.UnitTests/ResilientDirectoryTests.cs new file mode 100644 index 0000000..a1618cd --- /dev/null +++ b/src/Basic.CompilerLog.UnitTests/ResilientDirectoryTests.cs @@ -0,0 +1,68 @@ +using Basic.CompilerLog.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Basic.CompilerLog.UnitTests; + +public sealed class ResilientDirectoryTests +{ + public string RootPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? @"c:\" + : "/"; + + [Fact] + public void GetNewFilePathFlatten1() + { + using var tempDir = new TempDir(); + var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: true); + var path1 = dir.GetNewFilePath(Path.Combine(RootPath, "temp1", "resource.txt")); + var path2 = dir.GetNewFilePath(Path.Combine(RootPath, "temp2", "resource.txt")); + Assert.NotEqual(path1, path2); + Assert.Equal(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); + Assert.Equal("resource.txt", Path.GetFileName(path2)); + } + + [Fact] + public void GetNewFilePathFlatten2() + { + using var tempDir = new TempDir(); + var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: true); + var originalPath = Path.Combine(RootPath, "temp", "resource.txt"); + var path1 = dir.GetNewFilePath(originalPath); + var path2 = dir.GetNewFilePath(originalPath); + Assert.Equal(path1, path2); + Assert.Equal(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); + } + + [Fact] + public void GetNewFilePath1() + { + using var tempDir = new TempDir(); + var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: false); + var path1 = dir.GetNewFilePath(Path.Combine(RootPath, "temp1", "resource.txt")); + var path2 = dir.GetNewFilePath(Path.Combine(RootPath, "temp2", "resource.txt")); + Assert.NotEqual(path1, path2); + Assert.NotEqual(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); + Assert.NotEqual(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path2); + Assert.Equal("resource.txt", Path.GetFileName(path1)); + Assert.Equal("resource.txt", Path.GetFileName(path2)); + } + + [Fact] + public void GetNewFilePath2() + { + using var tempDir = new TempDir(); + var dir = new ResilientDirectory(tempDir.DirectoryPath, flatten: false); + var originalPath = Path.Combine(RootPath, "temp", "resource.txt"); + var path1 = dir.GetNewFilePath(originalPath); + var path2 = dir.GetNewFilePath(originalPath); + Assert.Equal(path1, path2); + Assert.NotEqual(Path.Combine(tempDir.DirectoryPath, "resource.txt"), path1); + Assert.Equal("resource.txt", Path.GetFileName(path1)); + } +} diff --git a/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs b/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs index fa03bba..027f7d2 100644 --- a/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs @@ -40,10 +40,13 @@ public void LoadAllWithAnalyzers() => public void LoadAllWithoutAnalyzers() => LoadAllCore(BasicAnalyzerHostOptions.None); - [Fact] - public async Task DocumentsHaveGeneratedTextWithAnalyzers() + [Theory] + [InlineData(BasicAnalyzerKind.Default)] + [InlineData(BasicAnalyzerKind.None)] + public async Task DocumentsHaveGeneratedTextWithAnalyzers(BasicAnalyzerKind kind) { - using var reader = SolutionReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.Default); + var host = new BasicAnalyzerHostOptions(kind); + using var reader = SolutionReader.Create(Fixture.ConsoleComplogPath, host); var workspace = new AdhocWorkspace(); var solution = workspace.AddSolution(reader.ReadSolutionInfo()); var project = solution.Projects.Single(); @@ -52,16 +55,4 @@ public async Task DocumentsHaveGeneratedTextWithAnalyzers() var doc = docs.First(x => x.Name == "RegexGenerator.g.cs"); Assert.NotNull(doc); } - - [Fact] - public void DocumentsHaveGeneratedTextWithoutAnalyzers() - { - using var reader = SolutionReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); - var workspace = new AdhocWorkspace(); - var solution = workspace.AddSolution(reader.ReadSolutionInfo()); - var project = solution.Projects.Single(); - Assert.Empty(project.AnalyzerReferences); - var doc = project.Documents.First(x => x.Name == "RegexGenerator.g.cs"); - Assert.NotNull(doc); - } } diff --git a/src/Basic.CompilerLog.UnitTests/TestBase.cs b/src/Basic.CompilerLog.UnitTests/TestBase.cs index b6601f7..a00d334 100644 --- a/src/Basic.CompilerLog.UnitTests/TestBase.cs +++ b/src/Basic.CompilerLog.UnitTests/TestBase.cs @@ -27,6 +27,7 @@ protected TestBase(ITestOutputHelper testOutputHelper, string name) public void Dispose() { + TestOutputHelper.WriteLine("Deleting temp directory"); Root.Dispose(); } diff --git a/src/Basic.CompilerLog.UnitTests/xunit.runner.json b/src/Basic.CompilerLog.UnitTests/xunit.runner.json index 9c8d3fe..f097e7c 100644 --- a/src/Basic.CompilerLog.UnitTests/xunit.runner.json +++ b/src/Basic.CompilerLog.UnitTests/xunit.runner.json @@ -1,4 +1,5 @@ { "appDomain": "ifAvailable", - "shadowCopy": false + "shadowCopy": false, + "parallelizeTestCollections": false } diff --git a/src/Basic.CompilerLog.Util/Basic.CompilerLog.Util.csproj b/src/Basic.CompilerLog.Util/Basic.CompilerLog.Util.csproj index 7a6fac2..f399503 100644 --- a/src/Basic.CompilerLog.Util/Basic.CompilerLog.Util.csproj +++ b/src/Basic.CompilerLog.Util/Basic.CompilerLog.Util.csproj @@ -5,6 +5,7 @@ enable enable true + $(NoWarn);RS2008 @@ -12,7 +13,7 @@ - + diff --git a/src/Basic.CompilerLog.Util/BasicAnalyzerHost.cs b/src/Basic.CompilerLog.Util/BasicAnalyzerHost.cs index a34eba9..098a801 100644 --- a/src/Basic.CompilerLog.Util/BasicAnalyzerHost.cs +++ b/src/Basic.CompilerLog.Util/BasicAnalyzerHost.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.ComponentModel; +using System.Diagnostics; #if NETCOREAPP using System.Runtime.Loader; @@ -117,9 +118,13 @@ public enum BasicAnalyzerKind OnDisk = 2, /// - /// Analyzers and generators are not loaded at all. The original generated files - /// will be loaded directly into the resulting . + /// Analyzers and generators from the original are not loaded at all. In the case + /// the original build had generated files they will be added through an in + /// memory analyzer that just adds them directly. /// + /// + /// This option avoids loading third party analyzers and generators. + /// None = 3, } diff --git a/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs b/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs index cbbe944..e532b41 100644 --- a/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs +++ b/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs @@ -223,123 +223,134 @@ private void AddSources(StreamWriter compilationWriter, CommandLineArguments arg /// private void AddGeneratedFiles(StreamWriter compilationWriter, CommandLineArguments args, CompilerCall compilerCall) { - // This only works when using portable and embedded pdb formats. A full PDB can't store - // generated files - if (args.EmitOptions.DebugInformationFormat is not (DebugInformationFormat.Embedded or DebugInformationFormat.PortablePdb)) - { - return; - } - - var assemblyFileName = GetAssemblyFileName(args); - var assemblyFilePath = Path.Combine(args.OutputDirectory, assemblyFileName); - if (!File.Exists(assemblyFilePath)) - { - Diagnostics.Add($"Can't find assembly file for {compilerCall.GetDiagnosticName()}"); - return; - } - var (languageGuid, languageExtension) = compilerCall.IsCSharp ? (LanguageTypeCSharp, ".cs") : (LanguageTypeBasic, ".vb"); - MetadataReaderProvider? pdbReaderProvider = null; - try + + var succeeded = AddGeneratedFilesCore(); + compilationWriter.WriteLine($"generatedResult:{(succeeded ? '1' : '0')}"); + + bool AddGeneratedFilesCore() { - using var reader = OpenFileForRead(assemblyFilePath); - using var peReader = new PEReader(reader); - if (!peReader.TryOpenAssociatedPortablePdb(assemblyFilePath, OpenPortablePdbFile, out pdbReaderProvider, out var pdbPath)) + // This only works when using portable and embedded pdb formats. A full PDB can't store + // generated files + if (args.EmitOptions.DebugInformationFormat is not (DebugInformationFormat.Embedded or DebugInformationFormat.PortablePdb)) { - Diagnostics.Add($"Can't find portable pdb file for {compilerCall.GetDiagnosticName()}"); - return; + Diagnostics.Add($"Can't read generated files from native PDB"); + return false; } - var pdbReader = pdbReaderProvider!.GetMetadataReader(); - foreach (var documentHandle in pdbReader.Documents.Skip(args.SourceFiles.Length)) + var assemblyFileName = GetAssemblyFileName(args); + var assemblyFilePath = Path.Combine(args.OutputDirectory, assemblyFileName); + if (!File.Exists(assemblyFilePath)) { - if (GetContentStream(documentHandle) is { } tuple) + Diagnostics.Add($"Can't find assembly file for {compilerCall.GetDiagnosticName()}"); + return false; + } + + MetadataReaderProvider? pdbReaderProvider = null; + try + { + using var reader = OpenFileForRead(assemblyFilePath); + using var peReader = new PEReader(reader); + if (!peReader.TryOpenAssociatedPortablePdb(assemblyFilePath, OpenPortablePdbFile, out pdbReaderProvider, out var pdbPath)) + { + Diagnostics.Add($"Can't find portable pdb file for {compilerCall.GetDiagnosticName()}"); + return false; + } + + var pdbReader = pdbReaderProvider!.GetMetadataReader(); + foreach (var documentHandle in pdbReader.Documents.Skip(args.SourceFiles.Length)) { - var contentHash = AddContent(tuple.Stream); - compilationWriter.WriteLine($"generated:{contentHash}:{tuple.Name}"); + if (GetContentStream(pdbReader, documentHandle) is { } tuple) + { + var contentHash = AddContent(tuple.Stream); + compilationWriter.WriteLine($"generated:{contentHash}:{tuple.FilePath}"); + } } + + return true; + + } + catch (Exception ex) + { + Diagnostics.Add($"Error embedding generated files {compilerCall.GetDiagnosticName()}): {ex.Message}"); + return false; } + finally + { + pdbReaderProvider?.Dispose(); + } + } - (string Name, MemoryStream Stream)? GetContentStream(DocumentHandle documentHandle) + (string FilePath, MemoryStream Stream)? GetContentStream(MetadataReader pdbReader, DocumentHandle documentHandle) + { + var document = pdbReader.GetDocument(documentHandle); + if (pdbReader.GetGuid(document.Language) != languageGuid) { - var document = pdbReader.GetDocument(documentHandle); - if (pdbReader.GetGuid(document.Language) != languageGuid) + return null; + } + + var filePath = pdbReader.GetString(document.Name); + + // A #line directive can be used to embed a file into the PDB. There is no way to differentiate + // between a file embedded this way and one generated from a source generator. For the moment + // using a simple hueristic to detect a generated file vs. say a .xaml file that was embedded + // https://github.com/jaredpar/basic-compilerlog/issues/45 + if (Path.GetExtension(filePath) != languageExtension) + { + return null; + } + + foreach (var cdiHandle in pdbReader.GetCustomDebugInformation(documentHandle)) + { + var cdi = pdbReader.GetCustomDebugInformation(cdiHandle); + if (pdbReader.GetGuid(cdi.Kind) != EmbeddedSourceGuid) { - return null; + continue; } - var name = pdbReader.GetString(document.Name); - - // A #line directive can be used to embed a file into the PDB. There is no way to differentiate - // between a file embedded this way and one generated from a source generator. For the moment - // using a simple hueristic to detect a generated file vs. say a .xaml file that was embedded - // https://github.com/jaredpar/basic-compilerlog/issues/45 - if (Path.GetExtension(name) != languageExtension) + var hashAlgorithmGuid = pdbReader.GetGuid(document.HashAlgorithm); + var hashAlgorithm = + hashAlgorithmGuid == HashAlgorithmSha1 ? SourceHashAlgorithm.Sha1 + : hashAlgorithmGuid == HashAlgorithmSha256 ? SourceHashAlgorithm.Sha256 + : SourceHashAlgorithm.None; + if (hashAlgorithm == SourceHashAlgorithm.None) { - return null; + continue; } - foreach (var cdiHandle in pdbReader.GetCustomDebugInformation(documentHandle)) + var bytes = pdbReader.GetBlobBytes(cdi.Value); + if (bytes is null) { - var cdi = pdbReader.GetCustomDebugInformation(cdiHandle); - if (pdbReader.GetGuid(cdi.Kind) != EmbeddedSourceGuid) - { - continue; - } + continue; + } - var hashAlgorithmGuid = pdbReader.GetGuid(document.HashAlgorithm); - var hashAlgorithm = - hashAlgorithmGuid == HashAlgorithmSha1 ? SourceHashAlgorithm.Sha1 - : hashAlgorithmGuid == HashAlgorithmSha256 ? SourceHashAlgorithm.Sha256 - : SourceHashAlgorithm.None; - if (hashAlgorithm == SourceHashAlgorithm.None) - { - continue; - } + int uncompressedSize = BitConverter.ToInt32(bytes, 0); + var stream = new MemoryStream(bytes, sizeof(int), bytes.Length - sizeof(int)); - var bytes = pdbReader.GetBlobBytes(cdi.Value); - if (bytes is null) + if (uncompressedSize != 0) + { + var decompressed = new MemoryStream(uncompressedSize); + using (var deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { - continue; + deflateStream.CopyTo(decompressed); } - int uncompressedSize = BitConverter.ToInt32(bytes, 0); - var stream = new MemoryStream(bytes, sizeof(int), bytes.Length - sizeof(int)); - - if (uncompressedSize != 0) + if (decompressed.Length != uncompressedSize) { - var decompressed = new MemoryStream(uncompressedSize); - using (var deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) - { - deflateStream.CopyTo(decompressed); - } - - if (decompressed.Length != uncompressedSize) - { - Diagnostics.Add($"Error decompressing embedded source file {compilerCall.GetDiagnosticName()}"); - continue; - } - - stream = decompressed; + Diagnostics.Add($"Error decompressing embedded source file {compilerCall.GetDiagnosticName()}"); + continue; } - stream.Position = 0; - return (name, stream); + stream = decompressed; } - return null; + stream.Position = 0; + return (filePath, stream); } - } - catch (Exception ex) - { - Diagnostics.Add($"Error embedding generated files {compilerCall.GetDiagnosticName()}): {ex.Message}"); - return; - } - finally - { - pdbReaderProvider?.Dispose(); + + return null; } // Similar to OpenFileForRead but don't throw here on file missing as it's expected that some files diff --git a/src/Basic.CompilerLog.Util/CompilerLogReader.cs b/src/Basic.CompilerLog.Util/CompilerLogReader.cs index db60364..627ea63 100644 --- a/src/Basic.CompilerLog.Util/CompilerLogReader.cs +++ b/src/Basic.CompilerLog.Util/CompilerLogReader.cs @@ -7,9 +7,11 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.VisualBasic; using System.Collections.Immutable; +using System.Diagnostics; using System.IO.Compression; using System.Reflection; using System.Reflection.Metadata; +using System.Runtime.CompilerServices; using System.Text; using static Basic.CompilerLog.Util.CommonUtil; @@ -140,10 +142,7 @@ public CompilationData ReadCompilationData(CompilerCall compilerCall) sourceTextList.Add((GetSourceText(tuple.ContentHash, hashAlgorithm), tuple.FilePath)); break; case RawContentKind.GeneratedText: - if (BasicAnalyzerHostOptions.ResolvedKind == BasicAnalyzerKind.None) - { - sourceTextList.Add((GetSourceText(tuple.ContentHash, hashAlgorithm), tuple.FilePath)); - } + // Handled when creating the analyzer host break; case RawContentKind.AnalyzerConfig: analyzerConfigList.Add((GetSourceText(tuple.ContentHash, hashAlgorithm), tuple.FilePath)); @@ -249,17 +248,7 @@ CSharpCompilationData CreateCSharp() var csharpArgs = (CSharpCommandLineArguments)rawCompilationData.Arguments; var csharpOptions = (CSharpCompilationOptions)compilationOptions; var parseOptions = csharpArgs.ParseOptions; - - var syntaxTrees = new SyntaxTree[sourceTextList.Count]; - Parallel.For( - 0, - sourceTextList.Count, - i => - { - var t = sourceTextList[i]; - syntaxTrees[i] = CSharpSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); - }); - + var syntaxTrees = ParseSourceTexts(sourceTextList); var (syntaxProvider, analyzerProvider) = CreateOptionsProviders(syntaxTrees, additionalTextList); csharpOptions = csharpOptions @@ -278,8 +267,27 @@ CSharpCompilationData CreateCSharp() emitData, csharpArgs, additionalTextList.ToImmutableArray(), - ReadAnalyzers(rawCompilationData.Analyzers), + ReadAnalyzers(rawCompilationData), analyzerProvider); + + SyntaxTree[] ParseSourceTexts(List<(SourceText SourceText, string Path)> sourceTextList) + { + if (sourceTextList.Count == 0) + { + return Array.Empty(); + } + + var syntaxTrees = new SyntaxTree[sourceTextList.Count]; + Parallel.For( + 0, + sourceTextList.Count, + i => + { + var t = sourceTextList[i]; + syntaxTrees[i] = CSharpSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); + }); + return syntaxTrees; + } } VisualBasicCompilationData CreateVisualBasic() @@ -287,16 +295,7 @@ VisualBasicCompilationData CreateVisualBasic() var basicArgs = (VisualBasicCommandLineArguments)rawCompilationData.Arguments; var basicOptions = (VisualBasicCompilationOptions)compilationOptions; var parseOptions = basicArgs.ParseOptions; - var syntaxTrees = new SyntaxTree[sourceTextList.Count]; - Parallel.For( - 0, - sourceTextList.Count, - i => - { - var t = sourceTextList[i]; - syntaxTrees[i] = VisualBasicSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); - }); - + var syntaxTrees = ParseSourceTexts(sourceTextList); var (syntaxProvider, analyzerProvider) = CreateOptionsProviders(syntaxTrees, additionalTextList); basicOptions = basicOptions @@ -315,8 +314,27 @@ VisualBasicCompilationData CreateVisualBasic() emitData, basicArgs, additionalTextList.ToImmutableArray(), - ReadAnalyzers(rawCompilationData.Analyzers), + ReadAnalyzers(rawCompilationData), analyzerProvider); + + SyntaxTree[] ParseSourceTexts(List<(SourceText SourceText, string Path)> sourceTextList) + { + if (sourceTextList.Count == 0) + { + return Array.Empty(); + } + + var syntaxTrees = new SyntaxTree[sourceTextList.Count]; + Parallel.For( + 0, + sourceTextList.Count, + i => + { + var t = sourceTextList[i]; + syntaxTrees[i] = VisualBasicSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); + }); + return syntaxTrees; + } } } @@ -362,6 +380,7 @@ internal RawCompilationData ReadRawCompilationData(CompilerCall compilerCall) var analyzers = new List(); var contents = new List<(string FilePath, string ContentHash, RawContentKind Kind)>(); var resources = new List(); + var readGeneratedFiles = false; while (reader.ReadLine() is string line) { @@ -380,6 +399,9 @@ internal RawCompilationData ReadRawCompilationData(CompilerCall compilerCall) case "generated": ParseContent(line, RawContentKind.GeneratedText); break; + case "generatedResult": + readGeneratedFiles = ParseBool(line); + break; case "config": ParseContent(line, RawContentKind.AnalyzerConfig); break; @@ -423,10 +445,17 @@ internal RawCompilationData ReadRawCompilationData(CompilerCall compilerCall) references, analyzers, contents, - resources); + resources, + readGeneratedFiles); return data; + bool ParseBool(string line) + { + var items = line.Split(':', count: 2); + return items[1] == "1"; + } + void ParseMetadataReference(string line) { var items = line.Split(':'); @@ -529,8 +558,9 @@ internal List ReadSourceContentHashes() return list; } - internal BasicAnalyzerHost ReadAnalyzers(List analyzers) + internal BasicAnalyzerHost ReadAnalyzers(RawCompilationData rawCompilationData) { + var analyzers = rawCompilationData.Analyzers; string? key = null; BasicAnalyzerHost? basicAnalyzerHost; if (BasicAnalyzerHostOptions.Cacheable) @@ -546,7 +576,7 @@ internal BasicAnalyzerHost ReadAnalyzers(List analyzers) { BasicAnalyzerKind.OnDisk => BasicAnalyzerHostOnDisk.Create(this, analyzers, BasicAnalyzerHostOptions), BasicAnalyzerKind.InMemory => BasicAnalyzerHostInMemory.Create(this, analyzers, BasicAnalyzerHostOptions), - BasicAnalyzerKind.None => new BasicAnalyzerHostNone(), + BasicAnalyzerKind.None => new BasicAnalyzerHostNone(rawCompilationData.ReadGeneratedFiles, ReadGeneratedSourceTexts()), _ => throw new InvalidOperationException() }; @@ -566,8 +596,29 @@ string GetKey() { builder.AppendLine($"{analyzer.Mvid}"); } + + if (BasicAnalyzerHostOptions.ResolvedKind == BasicAnalyzerKind.None) + { + foreach (var tuple in rawCompilationData.Contents.Where(static x => x.Kind == RawContentKind.GeneratedText)) + { + builder.AppendLine(tuple.ContentHash); + } + + builder.AppendLine(rawCompilationData.ReadGeneratedFiles.ToString()); + } + return builder.ToString(); } + + ImmutableArray<(SourceText SourceText, string Path)> ReadGeneratedSourceTexts() + { + var builder = ImmutableArray.CreateBuilder<(SourceText SourceText, string Path)>(); + foreach (var tuple in rawCompilationData.Contents.Where(static x => x.Kind == RawContentKind.GeneratedText)) + { + builder.Add((GetSourceText(tuple.ContentHash, rawCompilationData.Arguments.ChecksumAlgorithm), tuple.FilePath)); + } + return builder.ToImmutableArray(); + } } private static CompilerCall ReadCompilerCallCore(StreamReader reader, int index) diff --git a/src/Basic.CompilerLog.Util/ExportUtil.cs b/src/Basic.CompilerLog.Util/ExportUtil.cs index 3fe8995..5098a71 100644 --- a/src/Basic.CompilerLog.Util/ExportUtil.cs +++ b/src/Basic.CompilerLog.Util/ExportUtil.cs @@ -18,48 +18,6 @@ namespace Basic.CompilerLog.Util; /// public sealed class ExportUtil { - /// - /// Abstraction for getting new file paths for original paths in the compilation. - /// - private sealed class ResilientDirectory - { - /// - /// Content can exist outside the cone of the original project tree. That content - /// is mapped, by original directory name, to a new directory in the output. This - /// holds the map from the old directory to the new one. - /// - private Dictionary _map = new(PathUtil.Comparer); - - internal string DirectoryPath { get; } - - internal ResilientDirectory(string path) - { - DirectoryPath = path; - Directory.CreateDirectory(DirectoryPath); - } - - internal string GetNewFilePath(string originalFilePath) - { - var key = Path.GetDirectoryName(originalFilePath)!; - if (!_map.TryGetValue(key, out var dirPath)) - { - dirPath = Path.Combine(DirectoryPath, $"group{_map.Count}"); - Directory.CreateDirectory(dirPath); - _map[key] = dirPath; - } - - return Path.Combine(dirPath, Path.GetFileName(originalFilePath)); - } - - internal string WriteContent(string originalFilePath, Stream stream) - { - var newFilePath = GetNewFilePath(originalFilePath); - using var fileStream = new FileStream(newFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); - stream.CopyTo(fileStream); - return newFilePath; - } - } - private sealed class ContentBuilder { internal string DestinationDirectory { get; } @@ -84,7 +42,7 @@ internal ContentBuilder(string destinationDirectory, string originalProjectFileP MiscDirectory = new(Path.Combine(destinationDirectory, "misc")); GeneratedCodeDirectory = new(Path.Combine(destinationDirectory, "generated")); AnalyzerDirectory = new(Path.Combine(destinationDirectory, "analyzers")); - BuildOutput = new(Path.Combine(destinationDirectory, "output")); + BuildOutput = new(Path.Combine(destinationDirectory, "output"), flatten: true); Directory.CreateDirectory(SourceDirectory); Directory.CreateDirectory(EmbeddedResourceDirectory); } diff --git a/src/Basic.CompilerLog.Util/Impl/BasicAnalyzerHostNone.cs b/src/Basic.CompilerLog.Util/Impl/BasicAnalyzerHostNone.cs index 046134f..2e0b5f8 100644 --- a/src/Basic.CompilerLog.Util/Impl/BasicAnalyzerHostNone.cs +++ b/src/Basic.CompilerLog.Util/Impl/BasicAnalyzerHostNone.cs @@ -1,22 +1,110 @@ using System.Collections.Immutable; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; namespace Basic.CompilerLog.Util.Impl; /// -/// This is a per-compilation analyzer assembly loader that can be used to produce -/// instances +/// This is the analyzer host which doesn't actually run generators / analyzers. Instead it +/// uses the source texts that were generated at the original build time. /// internal sealed class BasicAnalyzerHostNone : BasicAnalyzerHost { - public BasicAnalyzerHostNone() - : base(BasicAnalyzerKind.None, ImmutableArray.Empty) - { + internal bool ReadGeneratedFiles { get; } + internal ImmutableArray<(SourceText SourceText, string Path)> GeneratedSourceTexts { get; } + + public static readonly DiagnosticDescriptor CannotReadGeneratedFiles = + new DiagnosticDescriptor( + "BCLA0001", + "Cannot read generated files", + "Generated files could not be read when compiler log was created", + "BasicCompilerLog", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + internal BasicAnalyzerHostNone(bool readGeneratedFiles, ImmutableArray<(SourceText SourceText, string Path)> generatedSourceTexts) + : base( + BasicAnalyzerKind.None, + CreateAnalyzerReferences(readGeneratedFiles, generatedSourceTexts)) + { + ReadGeneratedFiles = readGeneratedFiles; + GeneratedSourceTexts = generatedSourceTexts; } protected override void DisposeCore() { // Do nothing } -} \ No newline at end of file + + private static ImmutableArray CreateAnalyzerReferences(bool readGeneratedFiles, ImmutableArray<(SourceText SourceText, string Path)> generatedSourceTexts) => + readGeneratedFiles && generatedSourceTexts.Length == 0 + ? ImmutableArray.Empty + : ImmutableArray.Create(new NoneAnalyzerReference(readGeneratedFiles, generatedSourceTexts)); +} + +/// +/// Simple in memory generator for adding the pre-generated files in. +/// +/// +/// Note: this cannot be a file local type because of a Roslyn bug on .NET Framework +/// +internal sealed class NoneAnalyzerReference : AnalyzerReference, ISourceGenerator +{ + internal bool ReadGeneratedFiles { get; } + internal ImmutableArray<(SourceText SourceText, string Path)> GeneratedSourceTexts { get; } + public override object Id { get; } = Guid.NewGuid(); + public override string? FullPath => null; + + internal NoneAnalyzerReference(bool readGeneratedFiles, ImmutableArray<(SourceText SourceText, string Path)> generatedSourceTexts) + { + ReadGeneratedFiles = readGeneratedFiles; + GeneratedSourceTexts = generatedSourceTexts; + } + + public override ImmutableArray GetAnalyzers(string language) => + ImmutableArray.Empty; + + public override ImmutableArray GetAnalyzersForAllLanguages() => + ImmutableArray.Empty; + + public override ImmutableArray GetGenerators(string? language) => + ImmutableArray.Create(this); + + public override ImmutableArray GetGeneratorsForAllLanguages() => + ImmutableArray.Create(this); + + public override string ToString() => $"None"; + + public void Initialize(GeneratorInitializationContext context) + { + + } + + public void Execute(GeneratorExecutionContext context) + { + if (!ReadGeneratedFiles) + { + context.ReportDiagnostic(Diagnostic.Create( + BasicAnalyzerHostNone.CannotReadGeneratedFiles, + Location.None)); + } + + // The biggest challenge with adding names here is replicating the original + // hint name. There is no way to definitively recover the original name hence we + // have to go with just keeping the file name that was added then doing some basic + // counting to keep the name unique. + var set = new HashSet(PathUtil.Comparer); + foreach (var (sourceText, filePath) in GeneratedSourceTexts) + { + var fileName = Path.GetFileName(filePath); + if (!set.Add(fileName)) + { + fileName = Path.Combine(set.Count.ToString(), fileName); + } + + context.AddSource(fileName, sourceText); + } + } +} + diff --git a/src/Basic.CompilerLog.Util/RawCompilationData.cs b/src/Basic.CompilerLog.Util/RawCompilationData.cs index 5bbfdc5..b277fb6 100644 --- a/src/Basic.CompilerLog.Util/RawCompilationData.cs +++ b/src/Basic.CompilerLog.Util/RawCompilationData.cs @@ -73,17 +73,27 @@ internal sealed class RawCompilationData internal List<(string FilePath, string ContentHash, RawContentKind Kind)> Contents { get; } internal List Resources { get; } + /// + /// This is true when the generated files were successfully read from the original + /// compilation. This can be true when there are no generated files. A successful read + /// for example happens on a compilation where there are no analyzers (successfully + /// read zero files) + /// + internal bool ReadGeneratedFiles { get; } + internal RawCompilationData( CommandLineArguments arguments, List references, List analyzers, List<(string FilePath, string ContentHash, RawContentKind Kind)> contents, - List resources) + List resources, + bool readGeneratedFiles) { Arguments = arguments; References = references; Analyzers = analyzers; Contents = contents; Resources = resources; + ReadGeneratedFiles = readGeneratedFiles; } } diff --git a/src/Basic.CompilerLog.Util/ResilientDirectory.cs b/src/Basic.CompilerLog.Util/ResilientDirectory.cs new file mode 100644 index 0000000..6908d5b --- /dev/null +++ b/src/Basic.CompilerLog.Util/ResilientDirectory.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Basic.CompilerLog.Util; + +/// +/// Abstraction for getting new file paths for original paths in the compilation. +/// +internal sealed class ResilientDirectory +{ + /// + /// Content can exist outside the cone of the original project tree. That content + /// is mapped, by original directory name, to a new directory in the output. This + /// holds the map from the old directory to the new one. + /// + private Dictionary _map = new(PathUtil.Comparer); + + /// + /// When doing flattening this holds the map of file name that was flattened to the + /// path that it was flattened from. + /// + private Dictionary? _flattenedMap; + + internal string DirectoryPath { get; } + + /// + /// When true will attempt to flatten the directory structure by writing files + /// directly to the directory when possible. + /// + internal bool Flatten => _flattenedMap is not null; + + internal ResilientDirectory(string path, bool flatten = false) + { + DirectoryPath = path; + Directory.CreateDirectory(DirectoryPath); + if (flatten) + { + _flattenedMap = new(PathUtil.Comparer); + } + } + + internal string GetNewFilePath(string originalFilePath) + { + var fileName = Path.GetFileName(originalFilePath); + if (_flattenedMap is not null) + { + if (!_flattenedMap.TryGetValue(fileName, out var sourcePath) || + PathUtil.Comparer.Equals(sourcePath, originalFilePath)) + { + _flattenedMap[fileName] = originalFilePath; + return Path.Combine(DirectoryPath, fileName); + } + } + + var key = Path.GetDirectoryName(originalFilePath)!; + if (!_map.TryGetValue(key, out var dirPath)) + { + dirPath = Path.Combine(DirectoryPath, $"group{_map.Count}"); + Directory.CreateDirectory(dirPath); + _map[key] = dirPath; + } + + return Path.Combine(dirPath, fileName); + } + + internal string WriteContent(string originalFilePath, Stream stream) + { + var newFilePath = GetNewFilePath(originalFilePath); + using var fileStream = new FileStream(newFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + stream.CopyTo(fileStream); + return newFilePath; + } +} + diff --git a/src/Basic.CompilerLog.Util/SolutionReader.cs b/src/Basic.CompilerLog.Util/SolutionReader.cs index 4b2d2b3..de6d54a 100644 --- a/src/Basic.CompilerLog.Util/SolutionReader.cs +++ b/src/Basic.CompilerLog.Util/SolutionReader.cs @@ -1,5 +1,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -80,12 +81,7 @@ public ProjectInfo ReadProjectInfo(int index) Add(documents); break; case RawContentKind.GeneratedText: - // If we're not loading analyzers then we need to put the generated text into the - // project - if (Reader.BasicAnalyzerHostOptions.Kind == BasicAnalyzerKind.None) - { - Add(documents); - } + // Handled when creating analyzer host. break; case RawContentKind.AdditionalText: Add(additionalDocuments); @@ -122,7 +118,7 @@ void Add(List list) var projectReferences = new List(); var referenceList = Reader.GetMetadataReferences(FilterToUnique(rawCompilationData.References)); - var analyzers = Reader.ReadAnalyzers(rawCompilationData.Analyzers); + var analyzers = Reader.ReadAnalyzers(rawCompilationData); var projectInfo = ProjectInfo.Create( projectId, VersionStamp, diff --git a/src/Basic.CompilerLog/FilterOptionSet.cs b/src/Basic.CompilerLog/FilterOptionSet.cs index 3299657..f146551 100644 --- a/src/Basic.CompilerLog/FilterOptionSet.cs +++ b/src/Basic.CompilerLog/FilterOptionSet.cs @@ -7,16 +7,25 @@ internal sealed class FilterOptionSet : OptionSet internal bool IncludeAllKinds { get; set; } internal string? ProjectName { get; set; } internal bool Help { get; set; } + internal bool UseNoneHost { get; set; } - internal FilterOptionSet() + internal FilterOptionSet(bool includeNoneHost = false) { Add("include", "include all compilation kinds", i => { if (i != null) IncludeAllKinds = true; }); Add("targetframework=", "", TargetFrameworks.Add, hidden: true); Add("framework=", "include only compilations for the target framework (allows multiple)", TargetFrameworks.Add); Add("n|projectName=", "include only compilations with the project name", (string n) => ProjectName = n); Add("h|help", "print help", h => { if (h != null) Help = true; }); + + if (includeNoneHost) + { + Add("none", "do not use original analyzers / generators", n => { if (n != null) UseNoneHost = true; }); + } } + internal BasicAnalyzerHostOptions? CreateHostOptions() => + UseNoneHost ? BasicAnalyzerHostOptions.None : null; + internal bool FilterCompilerCalls(CompilerCall compilerCall) { if (compilerCall.Kind != CompilerCallKind.Regular && !IncludeAllKinds) diff --git a/src/Basic.CompilerLog/Program.cs b/src/Basic.CompilerLog/Program.cs index 752c26a..a0000ff 100644 --- a/src/Basic.CompilerLog/Program.cs +++ b/src/Basic.CompilerLog/Program.cs @@ -386,7 +386,7 @@ void PrintUsage() int RunEmit(IEnumerable args, CancellationToken cancellationToken) { var baseOutputPath = ""; - var options = new FilterOptionSet() + var options = new FilterOptionSet(includeNoneHost: true) { { "o|out=", "path to output binaries to", o => baseOutputPath = o }, }; @@ -452,7 +452,7 @@ void PrintUsage() int RunDiagnostics(IEnumerable args) { var severity = DiagnosticSeverity.Warning; - var options = new FilterOptionSet() + var options = new FilterOptionSet(includeNoneHost: true) { { "severity", "minimum severity to display (default Warning)", (DiagnosticSeverity s) => severity = s }, }; diff --git a/src/Scratch/Benchmarks.cs b/src/Scratch/Benchmarks.cs index a0c596c..03081a3 100644 --- a/src/Scratch/Benchmarks.cs +++ b/src/Scratch/Benchmarks.cs @@ -62,7 +62,7 @@ public void Emit() public void LoadAnalyzers() { using var reader = CompilerLogReader.Create(CompilerLogPath, options: new BasicAnalyzerHostOptions(Kind)); - var analyzers = reader.ReadAnalyzers(reader.ReadRawCompilationData(0).Item2.Analyzers); + var analyzers = reader.ReadAnalyzers(reader.ReadRawCompilationData(0).Item2); foreach (var analyzer in analyzers.AnalyzerReferences) { _ = analyzer.GetAnalyzersForAllLanguages();