diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs index db819e05020e1..26f33282dd270 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs @@ -2451,13 +2451,14 @@ private static void ValidateEmbeddedSources_Windows(Dictionary e continue; } - var sourceStr = Encoding.UTF8.GetString(sourceBlob.Array, sourceBlob.Offset, sourceBlob.Count); + // offset by 3 to skip the BOM + var sourceStr = Encoding.UTF8.GetString(sourceBlob.Array, sourceBlob.Offset + 3, sourceBlob.Count - 3); Assert.Equal(expectedEmbeddedMap[docPath], sourceStr); Assert.True(expectedEmbeddedMap.Remove(docPath)); } } - catch + finally { symReader?.Dispose(); } @@ -14324,6 +14325,123 @@ public void TestDuplicateAdditionalFiles(string additionalFilePath1, string addi [InlineData("abc/a.txt", "./../ABC/a.txt", 2)] public void TestDuplicateAdditionalFiles_Linux(string additionalFilePath1, string additionalFilePath2, int expectedCount) => TestDuplicateAdditionalFiles(additionalFilePath1, additionalFilePath2, expectedCount); + + [Theory] + [InlineData("/debug:embedded")] + [InlineData("/debug:portable")] + [InlineData("/debug:full")] + public void Generated_Trees_Have_Full_Path(string debugSwitch) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText("class C {}"); + var generatedDir = dir.CreateDirectory("generated"); + + var generatedSource = "public class D { }"; + var generator = new SingleFileTestGenerator(generatedSource, "generatedSource.cs"); + + VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { "/generatedfilesout:" + generatedDir.Path, debugSwitch, "/out:embed.exe" }, generators: new[] { generator }, analyzers: null); + + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + + ValidateWrittenSources(new() { { Path.Combine(generatedDir.Path, generatorPrefix), new() { { "generatedSource.cs", generatedSource } } } }); + + Dictionary expectedEmbeddedMap = new() { { Path.Combine(generatedDir.Path, generatorPrefix, "generatedSource.cs"), generatedSource } }; + switch (debugSwitch) + { + case "/debug:embedded": + ValidateEmbeddedSources_Portable(expectedEmbeddedMap, dir, isEmbeddedPdb: true); + break; + case "/debug:portable": + ValidateEmbeddedSources_Portable(expectedEmbeddedMap, dir, isEmbeddedPdb: false); + break; + case "/debug:full": + ValidateEmbeddedSources_Windows(expectedEmbeddedMap, dir); + break; + } + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + } + + [Theory] + [InlineData("/debug:embedded")] + [InlineData("/debug:portable")] + [InlineData("/debug:full")] + public void Generated_Trees_Have_Output_Path_When_No_GeneratedFilesOut(string debugSwitch) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText("class C {}"); + var generatedDir = dir.CreateDirectory("generated"); + + var generatedSource = "public class D { }"; + var generator = new SingleFileTestGenerator(generatedSource, "generatedSource.cs"); + + VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { debugSwitch, "/out:embed.exe" }, generators: new[] { generator }, analyzers: null); + + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + + // validate that *no* sources were written + Assert.Empty(Directory.GetDirectories(generatedDir.Path)); + + // but we still have full paths (using output dir) in the PDBs + Dictionary expectedEmbeddedMap = new() { { Path.Combine(dir.Path, generatorPrefix, "generatedSource.cs"), generatedSource } }; + switch (debugSwitch) + { + case "/debug:embedded": + ValidateEmbeddedSources_Portable(expectedEmbeddedMap, dir, isEmbeddedPdb: true); + break; + case "/debug:portable": + ValidateEmbeddedSources_Portable(expectedEmbeddedMap, dir, isEmbeddedPdb: false); + break; + case "/debug:full": + ValidateEmbeddedSources_Windows(expectedEmbeddedMap, dir); + break; + } + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + } + + [Theory] + [InlineData("/debug:embedded")] + [InlineData("/debug:portable")] + [InlineData("/debug:full")] + public void Generated_Trees_Output_Path_Respects_PathMap(string debugSwitch) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText("class C {}"); + var generatedDir = dir.CreateDirectory("generated"); + + var generatedSource = "public class D { }"; + var generator = new SingleFileTestGenerator(generatedSource, "generatedSource.cs"); + + var mappedPath = Path.Combine("mapped", "path"); + + VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, additionalFlags: new[] { debugSwitch, "/out:embed.exe", $"/pathmap:{dir.Path}={mappedPath}" }, generators: new[] { generator }, analyzers: null); + + var generatorPrefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + + // validate that *no* sources were written + Assert.Empty(Directory.GetDirectories(generatedDir.Path)); + + // but we still have full paths (using output dir) in the PDBs + Dictionary expectedEmbeddedMap = new() { { Path.Combine(mappedPath, generatorPrefix, "generatedSource.cs"), generatedSource } }; + switch (debugSwitch) + { + case "/debug:embedded": + ValidateEmbeddedSources_Portable(expectedEmbeddedMap, dir, isEmbeddedPdb: true); + break; + case "/debug:portable": + ValidateEmbeddedSources_Portable(expectedEmbeddedMap, dir, isEmbeddedPdb: false); + break; + case "/debug:full": + ValidateEmbeddedSources_Windows(expectedEmbeddedMap, dir); + break; + } + + // Clean up temp files + CleanupAllGeneratedFiles(src.Path); + } } [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] diff --git a/src/Compilers/CSharp/Test/CommandLine/SarifErrorLoggerTests.cs b/src/Compilers/CSharp/Test/CommandLine/SarifErrorLoggerTests.cs index 44f07f5579c65..a8ea93561bc58 100644 --- a/src/Compilers/CSharp/Test/CommandLine/SarifErrorLoggerTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/SarifErrorLoggerTests.cs @@ -9,6 +9,7 @@ using System.IO; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Test.Utilities.TestGenerators; using Xunit; using static Microsoft.CodeAnalysis.CommonDiagnosticAnalyzers; using static Roslyn.Test.Utilities.SharedResourceHelpers; @@ -23,6 +24,7 @@ public abstract class SarifErrorLoggerTests : CommandLineTestBase internal abstract string GetExpectedOutputForSimpleCompilerDiagnosticsSuppressed(CommonCompiler cmd, string sourceFile); internal abstract string GetExpectedOutputForAnalyzerDiagnosticsWithAndWithoutLocation(MockCSharpCompiler cmd); internal abstract string GetExpectedOutputForAnalyzerDiagnosticsWithSuppression(MockCSharpCompiler cmd, string justification); + internal abstract string GetExpectedOutputForSourceGenerator(CommonCompiler cmd, string sourceFile); protected void NoDiagnosticsImpl() { @@ -300,5 +302,36 @@ class C CleanupAllGeneratedFiles(sourceFile); CleanupAllGeneratedFiles(errorLogFile); } + + protected void SourceGeneratorImpl() + { + var source = "public class C { }"; + var dir = Temp.CreateDirectory(); + var sourceFile = dir.CreateFile("C.cs").WriteAllText(source).Path; + var generatedDir = dir.CreateDirectory("generated"); + var errorLogFile = Path.Combine(dir.Path, "ErrorLog.txt"); + var generator = new SingleFileTestGenerator("publiiic class D { }", "D.generated.cs"); + string[] arguments = new[] { "/nologo", sourceFile, "/preferreduilang:en", $"/errorlog:{errorLogFile}{ErrorLogQualifier}", "/generatedfilesout:" + generatedDir.Path }; + + var cmd = CreateCSharpCompiler(null, sourceFile, arguments, generators: ImmutableArray.Create(generator)); + + var outWriter = new StringWriter(CultureInfo.InvariantCulture); + + var exitCode = cmd.Run(outWriter); + var actualConsoleOutput = outWriter.ToString().Trim(); + + Assert.Contains("CS0116", actualConsoleOutput); + Assert.Contains("CS5001", actualConsoleOutput); + Assert.NotEqual(0, exitCode); + + var actualOutput = File.ReadAllText(errorLogFile).Trim(); + var prefix = GeneratorDriver.GetFilePathPrefixForGenerator(generator); + var expectedOutput = GetExpectedOutputForSourceGenerator(cmd, Path.Combine(generatedDir.Path, prefix, "D.generated.cs")); + + Assert.Equal(expectedOutput, actualOutput); + + CleanupAllGeneratedFiles(sourceFile); + CleanupAllGeneratedFiles(errorLogFile); + } } } diff --git a/src/Compilers/CSharp/Test/CommandLine/SarifV1ErrorLoggerTests.cs b/src/Compilers/CSharp/Test/CommandLine/SarifV1ErrorLoggerTests.cs index 7c07fbcc8a886..d0d4e235f266d 100644 --- a/src/Compilers/CSharp/Test/CommandLine/SarifV1ErrorLoggerTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/SarifV1ErrorLoggerTests.cs @@ -183,6 +183,77 @@ internal override string GetExpectedOutputForSimpleCompilerDiagnosticsSuppressed return expectedHeader + expectedIssues; } + [ConditionalFact(typeof(WindowsOnly), Reason = "https://github.com/dotnet/roslyn/issues/30289")] + public void SourceGenerator() + { + SourceGeneratorImpl(); + } + + internal override string GetExpectedOutputForSourceGenerator(CommonCompiler cmd, string sourceFile) + { + var expectedHeader = GetExpectedErrorLogHeader(cmd); + var expectedIssues = string.Format(@" + ""results"": [ + {{ + ""ruleId"": ""CS0116"", + ""level"": ""error"", + ""message"": ""A namespace cannot directly contain members such as fields, methods or statements"", + ""locations"": [ + {{ + ""resultFile"": {{ + ""uri"": ""{0}"", + ""region"": {{ + ""startLine"": 1, + ""startColumn"": 1, + ""endLine"": 1, + ""endColumn"": 9 + }} + }} + }} + ] + }}, + {{ + ""ruleId"": ""CS5001"", + ""level"": ""error"", + ""message"": ""Program does not contain a static 'Main' method suitable for an entry point"" + }} + ], + ""rules"": {{ + ""CS0116"": {{ + ""id"": ""CS0116"", + ""defaultLevel"": ""error"", + ""helpUri"": ""https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0116)"", + ""properties"": {{ + ""category"": ""Compiler"", + ""isEnabledByDefault"": true, + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ] + }} + }}, + ""CS5001"": {{ + ""id"": ""CS5001"", + ""defaultLevel"": ""error"", + ""helpUri"": ""https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS5001)"", + ""properties"": {{ + ""category"": ""Compiler"", + ""isEnabledByDefault"": true, + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ] + }} + }} + }} + }} + ] +}}", AnalyzerForErrorLogTest.GetUriForPath(sourceFile)); + return expectedHeader + expectedIssues; + } + [ConditionalFact(typeof(WindowsOnly), Reason = "https://github.com/dotnet/roslyn/issues/30289")] public void SimpleCompilerDiagnosticsSuppressed() { diff --git a/src/Compilers/CSharp/Test/CommandLine/SarifV2ErrorLoggerTests.cs b/src/Compilers/CSharp/Test/CommandLine/SarifV2ErrorLoggerTests.cs index 22e0797ea0aa4..fed8874e0127b 100644 --- a/src/Compilers/CSharp/Test/CommandLine/SarifV2ErrorLoggerTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/SarifV2ErrorLoggerTests.cs @@ -349,6 +349,103 @@ public void AnalyzerDiagnosticsSuppressedWithNullJustification() AnalyzerDiagnosticsSuppressedWithNullJustificationImpl(); } + [ConditionalFact(typeof(WindowsOnly))] + public void SourceGenerator() + { + SourceGeneratorImpl(); + } + + internal override string GetExpectedOutputForSourceGenerator(CommonCompiler cmd, string sourceFile) + { + var expectedOutput = @"{{ + ""$schema"": ""http://json.schemastore.org/sarif-2.1.0"", + ""version"": ""2.1.0"", + ""runs"": [ + {{ + ""results"": [ + {{ + ""ruleId"": ""CS0116"", + ""ruleIndex"": 0, + ""level"": ""error"", + ""message"": {{ + ""text"": ""A namespace cannot directly contain members such as fields, methods or statements"" + }}, + ""locations"": [ + {{ + ""physicalLocation"": {{ + ""artifactLocation"": {{ + ""uri"": ""{5}"" + }}, + ""region"": {{ + ""startLine"": 1, + ""startColumn"": 1, + ""endLine"": 1, + ""endColumn"": 9 + }} + }} + }} + ] + }}, + {{ + ""ruleId"": ""CS5001"", + ""ruleIndex"": 1, + ""level"": ""error"", + ""message"": {{ + ""text"": ""Program does not contain a static 'Main' method suitable for an entry point"" + }} + }} + ], + ""tool"": {{ + ""driver"": {{ + ""name"": ""{0}"", + ""version"": ""{1}"", + ""dottedQuadFileVersion"": ""{2}"", + ""semanticVersion"": ""{3}"", + ""language"": ""{4}"", + ""rules"": [ + {{ + ""id"": ""CS0116"", + ""defaultConfiguration"": {{ + ""level"": ""error"" + }}, + ""helpUri"": ""https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS0116)"", + ""properties"": {{ + ""category"": ""Compiler"", + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ] + }} + }}, + {{ + ""id"": ""CS5001"", + ""defaultConfiguration"": {{ + ""level"": ""error"" + }}, + ""helpUri"": ""https://msdn.microsoft.com/query/roslyn.query?appId=roslyn&k=k(CS5001)"", + ""properties"": {{ + ""category"": ""Compiler"", + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ] + }} + }} + ] + }} + }}, + ""columnKind"": ""utf16CodeUnits"" + }} + ] +}}"; + return FormatOutputText( + expectedOutput, + cmd, + AnalyzerForErrorLogTest.GetUriForPath(sourceFile)); + } + private string FormatOutputText( string s, CommonCompiler compiler, diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index 8baa2e86d268d..147d9ffd5cb75 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -749,8 +749,13 @@ private protected Compilation RunGenerators(Compilation input, ParseOptions pars } driver ??= CreateGeneratorDriver(parseOptions, generators, analyzerConfigOptionsProvider, additionalTexts); - driver = driver.RunGeneratorsAndUpdateCompilation(input, out var compilationOut, out var diagnostics); - generatorDiagnostics.AddRange(diagnostics); + + driver = driver.RunGenerators(input); + var results = driver.GetRunResult(); + + var treeRoot = this.Arguments.GeneratedFilesOutputDirectory ?? Arguments.OutputDirectory; + var compilationOut = input.AddSyntaxTrees(results.GeneratedTrees.Select(t => t.WithFilePath(Path.Combine(treeRoot, t.FilePath)))); + generatorDiagnostics.AddRange(results.Diagnostics); if (!disableCache) { @@ -1045,7 +1050,7 @@ private void CompileAndEmit( // write out the file if we have an output path if (hasGeneratedOutputPath) { - var path = Path.Combine(Arguments.GeneratedFilesOutputDirectory!, tree.FilePath); + var path = tree.FilePath; if (Directory.Exists(Arguments.GeneratedFilesOutputDirectory)) { Directory.CreateDirectory(Path.GetDirectoryName(path)!);