diff --git a/src/Basic.CompilerLog.UnitTests/BinaryLogReaderTests.cs b/src/Basic.CompilerLog.UnitTests/BinaryLogReaderTests.cs index f4eb5e1..6dc92da 100644 --- a/src/Basic.CompilerLog.UnitTests/BinaryLogReaderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/BinaryLogReaderTests.cs @@ -38,7 +38,7 @@ public void ReadCommandLineArgumentsOwnership() var compilerCall = reader.ReadAllCompilerCalls().First(); Assert.NotNull(reader.ReadCommandLineArguments(compilerCall)); - compilerCall = compilerCall.ChangeOwner(null); + compilerCall = compilerCall.WithOwner(null); Assert.Throws(() => reader.ReadCommandLineArguments(compilerCall)); } diff --git a/src/Basic.CompilerLog.UnitTests/CompilerCallTests.cs b/src/Basic.CompilerLog.UnitTests/CompilerCallTests.cs new file mode 100644 index 0000000..d32463b --- /dev/null +++ b/src/Basic.CompilerLog.UnitTests/CompilerCallTests.cs @@ -0,0 +1,22 @@ + +using Basic.CompilerLog.Util; +using Xunit; + +namespace Basic.CompilerLog.UnitTests; + +public class CompilerCallTests +{ + [Fact] + public void GetDiagnosticNameNoTargetFramework() + { + var compilerCall = new CompilerCall( + compilerFilePath: null, + "test.csproj", + CompilerCallKind.Regular, + targetFramework: null, + isCSharp: true, + arguments: new (() => [])); + Assert.Null(compilerCall.TargetFramework); + Assert.Equal(compilerCall.ProjectFileName, compilerCall.GetDiagnosticName()); + } +} \ No newline at end of file diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogBuilderTests.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogBuilderTests.cs index f9d5e94..b1799e6 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogBuilderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogBuilderTests.cs @@ -23,7 +23,7 @@ public void AddMissingFile() using var binlogStream = new FileStream(Fixture.ConsoleWithDiagnosticsBinaryLogPath, FileMode.Open, FileAccess.Read, FileShare.Read); var compilerCall = BinaryLogUtil.ReadAllCompilerCalls(binlogStream).First(x => x.IsCSharp); - compilerCall = compilerCall.ChangeArguments(["/sourcelink:does-not-exist.txt"]); + compilerCall = compilerCall.WithArguments(["/sourcelink:does-not-exist.txt"]); Assert.Throws(() => builder.AddFromDisk(compilerCall, BinaryLogUtil.ReadCommandLineArgumentsUnsafe(compilerCall))); } diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderExTests.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderExTests.cs index e904f02..419c58d 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderExTests.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderExTests.cs @@ -61,7 +61,7 @@ private CompilerLogReader ConvertConsoleArgs(Func, I ConvertConsole(x => { var args = func(x.GetArguments()); - return x.ChangeArguments(args); + return x.WithArguments(args); }, basicAnalyzerKind); [Fact] diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs index a53e2e0..083a82a 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs @@ -378,7 +378,7 @@ public void ReadCompilerCallWrongOwner() { using var reader = CompilerLogReader.Create(Fixture.Console.Value.CompilerLogPath); var compilerCall = reader.ReadCompilerCall(0); - compilerCall = compilerCall.ChangeOwner(null); + compilerCall = compilerCall.WithOwner(null); Assert.Throws(() => reader.ReadCompilationData(compilerCall)); } diff --git a/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs b/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs index bc900b6..606484a 100644 --- a/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs +++ b/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs @@ -1,4 +1,5 @@ using Basic.CompilerLog.Util; +using Basic.Reference.Assemblies; using System; using System.Collections.Generic; using System.Diagnostics.Contracts; @@ -45,6 +46,11 @@ private void TestExport(string compilerLogFilePath, int? expectedCount, bool inc internal static void TestExport(ITestOutputHelper testOutputHelper, string compilerLogFilePath, int? expectedCount, bool includeAnalyzers = true, Action? verifyExportCallback = null, bool runBuild = true) { using var reader = CompilerLogReader.Create(compilerLogFilePath); + TestExport(testOutputHelper, reader, expectedCount, includeAnalyzers, verifyExportCallback, runBuild); + } + + internal static void TestExport(ITestOutputHelper testOutputHelper, CompilerLogReader reader, int? expectedCount, bool includeAnalyzers = true, Action? verifyExportCallback = null, bool runBuild = true) + { #if NET var sdkDirs = SdkUtil.GetSdkDirectories(); #else @@ -65,7 +71,7 @@ internal static void TestExport(ITestOutputHelper testOutputHelper, string compi var buildResult = RunBuildCmd(tempDir.DirectoryPath); testOutputHelper.WriteLine(buildResult.StandardOut); testOutputHelper.WriteLine(buildResult.StandardError); - Assert.True(buildResult.Succeeded, $"Cannot build {Path.GetFileName(compilerLogFilePath)}"); + Assert.True(buildResult.Succeeded, $"Cannot build {compilerCall.ProjectFileName}"); } // Ensure that full paths aren't getting written out to the RSP file. That makes the @@ -182,6 +188,48 @@ public void ConsoleWithRuleset() }, runBuild: false); } + /// + /// Make sure that we can round trip a /link argument. That is a reference that we are embedding + /// interop types for. + /// + [Fact] + public void ConsoleWithLink() + { + var piaInfo = LibraryUtil.GetSimplePia(); + var linkFilePath = Root.NewFile(piaInfo.FileName, piaInfo.Image); + + using var reader = CreateReader(builder => + { + using var binlogReader = BinaryLogReader.Create(Fixture.Console.Value.BinaryLogPath!); + var compilerCall = binlogReader.ReadAllCompilerCalls().Single(); + string[] args = + [ + .. compilerCall.GetArguments(), + $"/link:{linkFilePath}" + ]; + compilerCall = compilerCall.WithArguments(args); + var commandLineArgs = binlogReader.ReadCommandLineArguments(compilerCall); + Assert.True(commandLineArgs.MetadataReferences.Any(x => x.Properties.EmbedInteropTypes)); + builder.AddFromDisk(compilerCall, commandLineArgs); + }); + + TestExport(TestOutputHelper, reader, expectedCount: 1, verifyExportCallback: tempPath => + { + var rspPath = Path.Combine(tempPath, "build.rsp"); + var foundPath = false; + foreach (var line in File.ReadAllLines(rspPath)) + { + if (line.StartsWith("/link:", StringComparison.Ordinal)) + { + foundPath = true; + Assert.Equal($@"/link:""ref{Path.DirectorySeparatorChar}{piaInfo.FileName}""", line); + } + } + + Assert.True(foundPath); + }, runBuild: true); + } + [Fact] public void StrongNameKey() { diff --git a/src/Basic.CompilerLog.UnitTests/Extensions.cs b/src/Basic.CompilerLog.UnitTests/Extensions.cs index f386378..2238411 100644 --- a/src/Basic.CompilerLog.UnitTests/Extensions.cs +++ b/src/Basic.CompilerLog.UnitTests/Extensions.cs @@ -39,7 +39,7 @@ internal static void ForEach(this IEnumerable enumerable, Action action } } - internal static CompilerCall ChangeArguments(this CompilerCall compilerCall, IReadOnlyCollection arguments) + internal static CompilerCall WithArguments(this CompilerCall compilerCall, IReadOnlyCollection arguments) { return new CompilerCall( compilerCall.CompilerFilePath, @@ -51,7 +51,7 @@ internal static CompilerCall ChangeArguments(this CompilerCall compilerCall, IRe compilerCall.OwnerState); } - internal static CompilerCall ChangeOwner(this CompilerCall compilerCall, object? ownerState) + internal static CompilerCall WithOwner(this CompilerCall compilerCall, object? ownerState) { var args = compilerCall.GetArguments(); return new CompilerCall( diff --git a/src/Basic.CompilerLog.UnitTests/LibraryUtil.cs b/src/Basic.CompilerLog.UnitTests/LibraryUtil.cs new file mode 100644 index 0000000..2f0aba6 --- /dev/null +++ b/src/Basic.CompilerLog.UnitTests/LibraryUtil.cs @@ -0,0 +1,70 @@ + +using System.Text; +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Basic.CompilerLog.UnitTests; + +/// +/// Class that builds helper libraries that are useful in tests +/// +internal static class LibraryUtil +{ + internal static (string FileName, MemoryStream Image) GetSimplePia() + { + var content1 = """ + using System.Runtime.InteropServices; + + [Guid("E8E4B023-7408-4A71-B3F6-ADEDE0A8FE11")] + [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] + public interface ICalculator + { + [DispId(1)] // COM method identifiers + int Add(int x, int y); + + [DispId(2)] + int Subtract(int x, int y); + } + + """; + + var content2 = """ + using System.Reflection; + using System.Runtime.InteropServices; + + [assembly: ComVisible(true)] + [assembly: Guid("12345678-90AB-CDEF-1234-567890ABCDEF")] + [assembly: PrimaryInteropAssembly(1, 0)] + """; + + var compilation = CSharpCompilation.Create( + "SimplePia", + [ CSharpSyntaxTree.ParseText(content1), CSharpSyntaxTree.ParseText(content2) ], + Net60.References.All, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var peStream = new MemoryStream(); + var emitResult = compilation.Emit(peStream); + if (!emitResult.Success) + { + throw new Exception(GetMessage(emitResult.Diagnostics)); + } + + peStream.Position = 0; + return ("SimplePia.dll", peStream); + + string GetMessage(IEnumerable diagnostics) + { + var builder = new StringBuilder(); + builder.AppendLine("Compilation failed with the following errors:"); + foreach (var d in diagnostics) + { + builder.AppendLine(d.ToString()); + } + return builder.ToString(); + } + + } + +} \ No newline at end of file diff --git a/src/Basic.CompilerLog.UnitTests/TempDir.cs b/src/Basic.CompilerLog.UnitTests/TempDir.cs index 77ba0e2..39d5e66 100644 --- a/src/Basic.CompilerLog.UnitTests/TempDir.cs +++ b/src/Basic.CompilerLog.UnitTests/TempDir.cs @@ -36,6 +36,14 @@ public string NewFile(string fileName, string content) return filePath; } + public string NewFile(string fileName, Stream content) + { + var filePath = Path.Combine(DirectoryPath, fileName); + using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + content.CopyTo(fileStream); + return filePath; + } + public string NewDirectory(string? name = null) { name ??= Guid.NewGuid().ToString(); diff --git a/src/Basic.CompilerLog.Util/ExportUtil.cs b/src/Basic.CompilerLog.Util/ExportUtil.cs index 2b30ff6..c075905 100644 --- a/src/Basic.CompilerLog.Util/ExportUtil.cs +++ b/src/Basic.CompilerLog.Util/ExportUtil.cs @@ -171,12 +171,13 @@ void WriteBuildCmd(string sdkDir, string cmdFileName) List ProcessRsp() { - var lines = new List(); + var arguments = compilerCall.GetArguments(); + var lines = new List(capacity: arguments.Count); // compiler options aren't case sensitive var comparison = StringComparison.OrdinalIgnoreCase; - foreach (var line in compilerCall.GetArguments()) + foreach (var line in arguments) { // The only non-options are source files and those are rewritten by other // methods and added to commandLineList @@ -196,7 +197,8 @@ List ProcessRsp() span.StartsWith("resource", comparison) || span.StartsWith("linkresource", comparison) || span.StartsWith("ruleset", comparison) || - span.StartsWith("keyfile", comparison)) + span.StartsWith("keyfile", comparison) || + span.StartsWith("link", comparison)) { continue; } @@ -250,6 +252,11 @@ void WriteReferences() commandLineList.Add(arg); } } + else if (tuple.EmbedInteropTypes) + { + var arg = $@"/link:""{PathUtil.RemovePathStart(filePath, destinationDir)}"""; + commandLineList.Add(arg); + } else { var arg = $@"/reference:""{PathUtil.RemovePathStart(filePath, destinationDir)}""";