From e4d465e2f47e16a6862deb1b8f8ef6bc33214bc6 Mon Sep 17 00:00:00 2001 From: Manish Vasani Date: Mon, 19 Aug 2019 19:03:59 -0700 Subject: [PATCH] Bail out from emit in presence of warnings reported as errors (#37948) Command line compilation is fixed to avoid writing any files to disk when compilation fails. The Emit API is changed to not write the PE file when compilation fails, but the behavior where the XML doc comments are still written is preserved. Fixes #37779 --- .../Test/CommandLine/CommandLineTests.cs | 162 +++++++++++++++ .../Test/Emit/Emit/CompilationEmitTests.cs | 91 ++++++++- .../Portable/CommandLine/CommonCompiler.cs | 60 ++++-- .../Core/Portable/Compilation/Compilation.cs | 9 +- .../Core/Portable/Diagnostic/Diagnostic.cs | 6 + .../DiagnosticAnalyzer/AnalyzerDriver.cs | 73 ++++--- .../Test/CommandLine/CommandLineTests.vb | 187 ++++++++++++++++++ 7 files changed, 543 insertions(+), 45 deletions(-) diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs index 64e9db01756..2d701abec16 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs @@ -11271,6 +11271,168 @@ class C { }"; CleanupAllGeneratedFiles(srcFile.Path); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void CompilerWarnAsErrorDoesNotEmit(bool warnAsError) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ + int _f; // CS0169: unused field +}"); + + var docName = "temp.xml"; + var pdbName = "temp.pdb"; + var additionalArgs = new[] { $"/doc:{docName}", $"/pdb:{pdbName}", "/debug" }; + if (warnAsError) + { + additionalArgs = additionalArgs.Append("/warnaserror").AsArray(); + } + + var expectedErrorCount = warnAsError ? 1 : 0; + var expectedWarningCount = !warnAsError ? 1 : 0; + var output = VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, + additionalArgs, + expectedErrorCount: expectedErrorCount, + expectedWarningCount: expectedWarningCount); + + var expectedOutput = warnAsError ? "error CS0169" : "warning CS0169"; + Assert.Contains(expectedOutput, output); + + string binaryPath = Path.Combine(dir.Path, "temp.dll"); + Assert.True(File.Exists(binaryPath) == !warnAsError); + + string pdbPath = Path.Combine(dir.Path, pdbName); + Assert.True(File.Exists(pdbPath) == !warnAsError); + + string xmlDocFilePath = Path.Combine(dir.Path, docName); + Assert.True(File.Exists(xmlDocFilePath) == !warnAsError); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void AnalyzerConfigSeverityEscalationToErrorDoesNotEmit(bool analyzerConfigSetToError) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ + int _f; // CS0169: unused field +}"); + + var docName = "temp.xml"; + var pdbName = "temp.pdb"; + var additionalArgs = new[] { $"/doc:{docName}", $"/pdb:{pdbName}", "/debug" }; + + if (analyzerConfigSetToError) + { + var analyzerConfig = dir.CreateFile(".editorconfig").WriteAllText(@" +[*.cs] +dotnet_diagnostic.cs0169.severity = error"); + + additionalArgs = additionalArgs.Append("/analyzerconfig:" + analyzerConfig.Path).ToArray(); + } + + var expectedErrorCount = analyzerConfigSetToError ? 1 : 0; + var expectedWarningCount = !analyzerConfigSetToError ? 1 : 0; + var output = VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, + additionalArgs, + expectedErrorCount: expectedErrorCount, + expectedWarningCount: expectedWarningCount); + + var expectedOutput = analyzerConfigSetToError ? "error CS0169" : "warning CS0169"; + Assert.Contains(expectedOutput, output); + + string binaryPath = Path.Combine(dir.Path, "temp.dll"); + Assert.True(File.Exists(binaryPath) == !analyzerConfigSetToError); + + string pdbPath = Path.Combine(dir.Path, pdbName); + Assert.True(File.Exists(pdbPath) == !analyzerConfigSetToError); + + string xmlDocFilePath = Path.Combine(dir.Path, docName); + Assert.True(File.Exists(xmlDocFilePath) == !analyzerConfigSetToError); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void RulesetSeverityEscalationToErrorDoesNotEmit(bool rulesetSetToError) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText(@" +class C +{ + int _f; // CS0169: unused field +}"); + + var docName = "temp.xml"; + var pdbName = "temp.pdb"; + var additionalArgs = new[] { $"/doc:{docName}", $"/pdb:{pdbName}", "/debug" }; + + if (rulesetSetToError) + { + string source = @" + + + + + +"; + var rulesetFile = CreateRuleSetFile(source); + additionalArgs = additionalArgs.Append("/ruleset:" + rulesetFile.Path).ToArray(); + } + + var expectedErrorCount = rulesetSetToError ? 1 : 0; + var expectedWarningCount = !rulesetSetToError ? 1 : 0; + var output = VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, + additionalArgs, + expectedErrorCount: expectedErrorCount, + expectedWarningCount: expectedWarningCount); + + var expectedOutput = rulesetSetToError ? "error CS0169" : "warning CS0169"; + Assert.Contains(expectedOutput, output); + + string binaryPath = Path.Combine(dir.Path, "temp.dll"); + Assert.True(File.Exists(binaryPath) == !rulesetSetToError); + + string pdbPath = Path.Combine(dir.Path, pdbName); + Assert.True(File.Exists(pdbPath) == !rulesetSetToError); + + string xmlDocFilePath = Path.Combine(dir.Path, docName); + Assert.True(File.Exists(xmlDocFilePath) == !rulesetSetToError); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void AnalyzerWarnAsErrorDoesNotEmit(bool warnAsError) + { + var dir = Temp.CreateDirectory(); + var src = dir.CreateFile("temp.cs").WriteAllText("class C { }"); + + var additionalArgs = warnAsError ? new[] { "/warnaserror" } : null; + var expectedErrorCount = warnAsError ? 1 : 0; + var expectedWarningCount = !warnAsError ? 1 : 0; + var output = VerifyOutput(dir, src, includeCurrentAssemblyAsAnalyzerReference: false, + additionalArgs, + expectedErrorCount: expectedErrorCount, + expectedWarningCount: expectedWarningCount, + analyzers: new[] { new WarningDiagnosticAnalyzer() }); + + var expectedDiagnosticSeverity = warnAsError ? "error" : "warning"; + Assert.Contains($"{expectedDiagnosticSeverity} {WarningDiagnosticAnalyzer.Warning01.Id}", output); + + string binaryPath = Path.Combine(dir.Path, "temp.dll"); + Assert.True(File.Exists(binaryPath) == !warnAsError); + } } [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] diff --git a/src/Compilers/CSharp/Test/Emit/Emit/CompilationEmitTests.cs b/src/Compilers/CSharp/Test/Emit/Emit/CompilationEmitTests.cs index e12b3e17a0a..70419535560 100644 --- a/src/Compilers/CSharp/Test/Emit/Emit/CompilationEmitTests.cs +++ b/src/Compilers/CSharp/Test/Emit/Emit/CompilationEmitTests.cs @@ -18,7 +18,6 @@ using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; -using static Roslyn.Test.Utilities.SigningTestHelpers; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Emit { @@ -5163,5 +5162,95 @@ public void CompileAndVerifyModuleIncludesAllModules() Assert.Equal("refMod.netmodule", a.ContainingModule.Name); }); } + + [Fact] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void WarnAsErrorDoesNotEmit_GeneralDiagnosticOption() + { + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, generalDiagnosticOption: ReportDiagnostic.Error); + TestWarnAsErrorDoesNotEmitCore(options); + } + + [Fact] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void WarnAsErrorDoesNotEmit_SpecificDiagnosticOption() + { + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithSpecificDiagnosticOptions("CS0169", ReportDiagnostic.Error); + TestWarnAsErrorDoesNotEmitCore(options); + } + + private void TestWarnAsErrorDoesNotEmitCore(CSharpCompilationOptions options) + { + string source = @" +class X +{ + int _f; +}"; + var compilation = CreateCompilation(source, options: options); + + using var output = new MemoryStream(); + using var pdbStream = new MemoryStream(); + using var xmlDocumentationStream = new MemoryStream(); + using var win32ResourcesStream = compilation.CreateDefaultWin32Resources(versionResource: true, noManifest: false, manifestContents: null, iconInIcoFormat: null); + + var emitResult = compilation.Emit(output, pdbStream, xmlDocumentationStream, win32ResourcesStream); + Assert.False(emitResult.Success); + + Assert.Equal(0, output.Length); + Assert.Equal(0, pdbStream.Length); + + // https://github.com/dotnet/roslyn/issues/37996 tracks revisiting the below behavior. + Assert.True(xmlDocumentationStream.Length > 0); + + emitResult.Diagnostics.Verify( + // (4,9): error CS0169: The field 'X._f' is never used + // int _f; + Diagnostic(ErrorCode.WRN_UnreferencedField, "_f").WithArguments("X._f").WithLocation(4, 9).WithWarningAsError(true)); + } + + [Fact] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void WarnAsErrorWithMetadataOnlyImageDoesEmit_GeneralDiagnosticOption() + { + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, generalDiagnosticOption: ReportDiagnostic.Error); + TestWarnAsErrorWithMetadataOnlyImageDoesEmitCore(options); + } + + [Fact] + [WorkItem(37779, "https://github.com/dotnet/roslyn/issues/37779")] + public void WarnAsErrorWithMetadataOnlyImageDoesEmit_SpecificDiagnosticOptions() + { + var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary).WithSpecificDiagnosticOptions("CS0612", ReportDiagnostic.Error); + TestWarnAsErrorWithMetadataOnlyImageDoesEmitCore(options); + } + + private void TestWarnAsErrorWithMetadataOnlyImageDoesEmitCore(CSharpCompilationOptions options) + { + string source = @" +public class X +{ + public void M(Y y) + { + } +} + +[System.Obsolete] +public class Y { } +"; + var compilation = CreateCompilation(source, options: options); + + using var output = new MemoryStream(); + var emitOptions = new EmitOptions(metadataOnly: true); + + var emitResult = compilation.Emit(output, options: emitOptions); + Assert.True(emitResult.Success); + + Assert.True(output.Length > 0); + + emitResult.Diagnostics.Verify( + // (4,19): error CS0612: 'Y' is obsolete + // public void M(Y y) + Diagnostic(ErrorCode.WRN_DeprecatedSymbol, "Y").WithArguments("Y").WithLocation(4, 19).WithWarningAsError(true)); + } } } diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index acac0a78b76..131972003c8 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -16,8 +16,6 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; -using static Microsoft.CodeAnalysis.AnalyzerConfig; -using TreeOptions = System.Collections.Immutable.ImmutableDictionary; namespace Microsoft.CodeAnalysis { @@ -564,11 +562,8 @@ internal bool ReportDiagnostics(IEnumerable diagnostics, TextWri /// are guaranteed to break the build. /// Only diagnostics which have default severity error and are tagged as NotConfigurable fall in this bucket. /// This includes all compiler error diagnostics and specific analyzer error diagnostics that are marked as not configurable by the analyzer author. - /// Note: does NOT do filtering, so it may return false if a - /// non-error diagnostic were later elevated to an error through filtering (e.g., through - /// warn-as-error). /// - internal static bool HasUnsuppressedErrors(DiagnosticBag diagnostics) + internal static bool HasUnsuppressableErrors(DiagnosticBag diagnostics) { foreach (var diag in diagnostics.AsEnumerable()) { @@ -580,6 +575,23 @@ internal static bool HasUnsuppressedErrors(DiagnosticBag diagnostics) return false; } + /// + /// Returns true if the bag has any diagnostics with effective Severity=Error. Also returns true for warnings or informationals + /// or warnings promoted to error via /warnaserror which are not suppressed. + /// + internal static bool HasUnsuppressedErrors(DiagnosticBag diagnostics) + { + foreach (Diagnostic diagnostic in diagnostics.AsEnumerable()) + { + if (diagnostic.IsUnsuppressedError) + { + return true; + } + } + + return false; + } + protected virtual void PrintError(Diagnostic diagnostic, TextWriter consoleOutput) { consoleOutput.WriteLine(DiagnosticFormatter.Format(diagnostic, Culture)); @@ -843,7 +855,7 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat // Print the diagnostics produced during the parsing stage and exit if there were any errors. compilation.GetDiagnostics(CompilationStage.Parse, includeEarlierStages: false, diagnostics, cancellationToken); - if (HasUnsuppressedErrors(diagnostics)) + if (HasUnsuppressableErrors(diagnostics)) { return; } @@ -891,7 +903,7 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat } compilation.GetDiagnostics(CompilationStage.Declare, includeEarlierStages: false, diagnostics, cancellationToken); - if (HasUnsuppressedErrors(diagnostics)) + if (HasUnsuppressableErrors(diagnostics)) { return; } @@ -964,6 +976,22 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat filterOpt: null, cancellationToken: cancellationToken); + // Prior to generating the xml documentation file, + // we apply programmatic suppressions for compiler warnings from diagnostic suppressors. + // If there are still any unsuppressed errors or warnings escalated to errors + // then we bail out from generating the documentation file. + // This maintains the compiler invariant that xml documentation file should not be + // generated in presence of diagnostics that break the build. + if (analyzerDriver != null && !diagnostics.IsEmptyWithoutResolution) + { + analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation); + } + + if (HasUnsuppressedErrors(diagnostics)) + { + success = false; + } + if (success) { // NOTE: as native compiler does, we generate the documentation file @@ -1003,7 +1031,7 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat { using (var win32ResourceStreamOpt = GetWin32Resources(MessageProvider, Arguments, compilation, diagnostics)) { - if (HasUnsuppressedErrors(diagnostics)) + if (HasUnsuppressableErrors(diagnostics)) { return; } @@ -1044,11 +1072,6 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat { // Apply diagnostic suppressions for analyzer and/or compiler diagnostics from diagnostic suppressors. analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation); - - if (HasUnsuppressedErrors(diagnostics)) - { - success = false; - } } } } @@ -1057,6 +1080,11 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat moduleBeingBuilt.CompilationFinished(); } + if (HasUnsuppressedErrors(diagnostics)) + { + success = false; + } + if (success) { var peStreamProvider = new CompilerEmitStreamProvider(this, finalPeFilePath); @@ -1104,7 +1132,7 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat } } - if (HasUnsuppressedErrors(diagnostics)) + if (HasUnsuppressableErrors(diagnostics)) { return; } @@ -1124,7 +1152,7 @@ private int RunCore(TextWriter consoleOutput, ErrorLogger errorLogger, Cancellat if (analyzerExceptionDiagnostics != null) { diagnostics.AddRange(analyzerExceptionDiagnostics); - if (HasUnsuppressedErrors(analyzerExceptionDiagnostics)) + if (HasUnsuppressableErrors(analyzerExceptionDiagnostics)) { return; } diff --git a/src/Compilers/Core/Portable/Compilation/Compilation.cs b/src/Compilers/Core/Portable/Compilation/Compilation.cs index 99c2b28593a..a5cfdf601ea 100644 --- a/src/Compilers/Core/Portable/Compilation/Compilation.cs +++ b/src/Compilers/Core/Portable/Compilation/Compilation.cs @@ -2489,6 +2489,8 @@ internal void EnsureAnonymousTypeTemplates(CancellationToken cancellationToken) if (!options.EmitMetadataOnly) { + // NOTE: We generate documentation even in presence of compile errors. + // https://github.com/dotnet/roslyn/issues/37996 tracks revisiting this behavior. if (!GenerateResourcesAndDocumentationComments( moduleBeingBuilt, xmlDocumentationStream, @@ -2517,6 +2519,11 @@ internal void EnsureAnonymousTypeTemplates(CancellationToken cancellationToken) privateKeyOpt = StrongNameKeys.PrivateKey; } + if (!options.EmitMetadataOnly && CommonCompiler.HasUnsuppressedErrors(diagnostics)) + { + success = false; + } + if (success) { success = SerializeToPeStream( @@ -2654,7 +2661,7 @@ internal void EnsureAnonymousTypeTemplates(CancellationToken cancellationToken) } } - if (CommonCompiler.HasUnsuppressedErrors(diagnostics)) + if (CommonCompiler.HasUnsuppressableErrors(diagnostics)) { return null; } diff --git a/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs b/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs index e784965d84d..3b8a38c0db7 100644 --- a/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs +++ b/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs @@ -567,6 +567,12 @@ internal virtual bool IsNotConfigurable() /// internal bool IsUnsuppressableError() => DefaultSeverity == DiagnosticSeverity.Error && IsNotConfigurable(); + + /// + /// Returns true if this is a unsuppressed diagnostic with an effective error severity. + /// + internal bool IsUnsuppressedError + => Severity == DiagnosticSeverity.Error && !IsSuppressed; } /// diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs index b26f06c2e91..884d35cb038 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs @@ -35,6 +35,11 @@ internal abstract partial class AnalyzerDriver : IDisposable /// private readonly ConcurrentSet _programmaticSuppressions; + /// + /// Set of diagnostics that have already been processed for application of programmatic suppressions. + /// + private readonly ConcurrentSet _diagnosticsProcessedForProgrammaticSuppressions; + /// /// Flag indicating if the include any /// which can suppress reported analyzer/compiler diagnostics. @@ -168,6 +173,7 @@ protected AnalyzerDriver(ImmutableArray analyzers, AnalyzerM _isGeneratedCode = (tree, ct) => GeneratedCodeUtilities.IsGeneratedCode(tree, isComment, ct); _hasDiagnosticSuppressors = this.Analyzers.Any(a => a is DiagnosticSuppressor); _programmaticSuppressions = _hasDiagnosticSuppressors ? new ConcurrentSet() : null; + _diagnosticsProcessedForProgrammaticSuppressions = _hasDiagnosticSuppressors ? new ConcurrentSet(ReferenceEqualityComparer.Instance) : null; } /// @@ -631,42 +637,55 @@ private ImmutableArray ApplyProgrammaticSuppressionsCore(ImmutableAr Debug.Assert(_hasDiagnosticSuppressors); Debug.Assert(!reportedDiagnostics.IsEmpty); Debug.Assert(_programmaticSuppressions != null); + Debug.Assert(_diagnosticsProcessedForProgrammaticSuppressions != null); - // We do not allow analyzer based suppressions for following category of diagnostics: - // 1. Diagnostics which are already suppressed in source via pragma/suppress message attribute. - // 2. Diagnostics explicitly tagged as not configurable by analyzer authors - this includes compiler error diagnostics. - // 3. Diagnostics which are marked as error by default by diagnostic authors. - var suppressableDiagnostics = reportedDiagnostics.Where(d => !d.IsSuppressed && - !d.IsNotConfigurable() && - d.DefaultSeverity != DiagnosticSeverity.Error); - if (suppressableDiagnostics.IsEmpty()) + try { - return reportedDiagnostics; - } + // We do not allow analyzer based suppressions for following category of diagnostics: + // 1. Diagnostics which are already suppressed in source via pragma/suppress message attribute. + // 2. Diagnostics explicitly tagged as not configurable by analyzer authors - this includes compiler error diagnostics. + // 3. Diagnostics which are marked as error by default by diagnostic authors. + var suppressableDiagnostics = reportedDiagnostics.Where(d => !d.IsSuppressed && + !d.IsNotConfigurable() && + d.DefaultSeverity != DiagnosticSeverity.Error && + !_diagnosticsProcessedForProgrammaticSuppressions.Contains(d)); - executeSuppressionActions(suppressableDiagnostics, concurrent: compilation.Options.ConcurrentBuild); - if (_programmaticSuppressions.IsEmpty) - { - return reportedDiagnostics; - } + if (suppressableDiagnostics.IsEmpty()) + { + return reportedDiagnostics; + } - var builder = ArrayBuilder.GetInstance(reportedDiagnostics.Length); - ImmutableDictionary programmaticSuppressionsByDiagnostic = createProgrammaticSuppressionsByDiagnosticMap(_programmaticSuppressions); - foreach (var diagnostic in reportedDiagnostics) - { - if (programmaticSuppressionsByDiagnostic.TryGetValue(diagnostic, out var programmaticSuppressionInfo)) + executeSuppressionActions(suppressableDiagnostics, concurrent: compilation.Options.ConcurrentBuild); + if (_programmaticSuppressions.IsEmpty) { - Debug.Assert(suppressableDiagnostics.Contains(diagnostic)); - Debug.Assert(!diagnostic.IsSuppressed); - builder.Add(diagnostic.WithProgrammaticSuppression(programmaticSuppressionInfo)); + return reportedDiagnostics; } - else + + var builder = ArrayBuilder.GetInstance(reportedDiagnostics.Length); + ImmutableDictionary programmaticSuppressionsByDiagnostic = createProgrammaticSuppressionsByDiagnosticMap(_programmaticSuppressions); + foreach (var diagnostic in reportedDiagnostics) { - builder.Add(diagnostic); + if (programmaticSuppressionsByDiagnostic.TryGetValue(diagnostic, out var programmaticSuppressionInfo)) + { + Debug.Assert(suppressableDiagnostics.Contains(diagnostic)); + Debug.Assert(!diagnostic.IsSuppressed); + var suppressedDiagnostic = diagnostic.WithProgrammaticSuppression(programmaticSuppressionInfo); + Debug.Assert(suppressedDiagnostic.IsSuppressed); + builder.Add(suppressedDiagnostic); + } + else + { + builder.Add(diagnostic); + } } - } - return builder.ToImmutableAndFree(); + return builder.ToImmutableAndFree(); + } + finally + { + // Mark the reported diagnostics as processed for programmatic suppressions to avoid duplicate callbacks to suppressors for same diagnostics. + _diagnosticsProcessedForProgrammaticSuppressions.AddRange(reportedDiagnostics); + } void executeSuppressionActions(IEnumerable reportedDiagnostics, bool concurrent) { diff --git a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb index 9e087a02c0f..2ae614f62a6 100644 --- a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb +++ b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb @@ -9554,6 +9554,193 @@ End Class" CleanupAllGeneratedFiles(file.Path) End Sub + + + + + + Public Sub CompilerWarnAsErrorDoesNotEmit(ByVal warnAsError As Boolean) + ' warning BC40008 : 'C' is obsolete + Dim source = " +Imports System + + +Class C +End Class + +Class D + Inherits C +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("temp.vb") + file.WriteAllText(source) + + Dim docName As String = "doc.xml" + Dim additionalFlags = {$"/doc:{docName}", "/debug:full"} + If warnAsError Then + additionalFlags = additionalFlags.Append("/warnaserror").AsArray() + End If + + Dim expectedErrorCount = If(warnAsError, 1, 0) + Dim expectedWarningCount = If(Not warnAsError, 1, 0) + Dim output = VerifyOutput(dir, file, + includeCurrentAssemblyAsAnalyzerReference:=False, + additionalFlags, + expectedErrorCount:=expectedErrorCount, + expectedWarningCount:=expectedWarningCount) + + Dim expectedOutput = If(warnAsError, "error BC40008", "warning BC40008") + Assert.Contains(expectedOutput, output) + + Dim binaryPath As String = Path.Combine(dir.Path, "temp.dll") + Assert.True(IO.File.Exists(binaryPath) = Not warnAsError) + + Dim pdbPath As String = Path.Combine(dir.Path, "temp.pdb") + Assert.True(IO.File.Exists(pdbPath) = Not warnAsError) + + Dim docPath As String = Path.Combine(dir.Path, docName) + Assert.True(IO.File.Exists(docPath) = Not warnAsError) + End Sub + + + + + + Public Sub AnalyzerConfigSeverityEscalationToErrorDoesNotEmit(ByVal analyzerConfigSetToError As Boolean) + ' warning BC40008 : 'C' is obsolete + Dim source = " +Imports System + + +Class C +End Class + +Class D + Inherits C +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("temp.vb") + file.WriteAllText(source) + + Dim docName As String = "doc.xml" + Dim additionalFlags = {$"/doc:{docName}", "/debug:full"} + + If analyzerConfigSetToError Then + Dim analyzerConfig = dir.CreateFile(".editorconfig").WriteAllText(" +[*.vb] +dotnet_diagnostic.bc40008.severity = error") + + additionalFlags = additionalFlags.Append("/analyzerconfig:" + analyzerConfig.Path).ToArray() + End If + + Dim expectedErrorCount = If(analyzerConfigSetToError, 1, 0) + Dim expectedWarningCount = If(Not analyzerConfigSetToError, 1, 0) + Dim output = VerifyOutput(dir, file, + includeCurrentAssemblyAsAnalyzerReference:=False, + additionalFlags, + expectedErrorCount:=expectedErrorCount, + expectedWarningCount:=expectedWarningCount) + + Dim expectedOutput = If(analyzerConfigSetToError, "error BC40008", "warning BC40008") + Assert.Contains(expectedOutput, output) + + Dim binaryPath As String = Path.Combine(dir.Path, "temp.dll") + Assert.True(IO.File.Exists(binaryPath) = Not analyzerConfigSetToError) + + Dim pdbPath As String = Path.Combine(dir.Path, "temp.pdb") + Assert.True(IO.File.Exists(pdbPath) = Not analyzerConfigSetToError) + + Dim docPath As String = Path.Combine(dir.Path, docName) + Assert.True(IO.File.Exists(docPath) = Not analyzerConfigSetToError) + End Sub + + + + + + Public Sub RulesetSeverityEscalationToErrorDoesNotEmit(ByVal rulesetSetToError As Boolean) + ' warning BC40008 : 'C' is obsolete + Dim source = " +Imports System + + +Class C +End Class + +Class D + Inherits C +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("temp.vb") + file.WriteAllText(source) + + Dim docName As String = "doc.xml" + Dim additionalFlags = {$"/doc:{docName}", "/debug:full"} + + If rulesetSetToError Then + Dim rulesetSource = + + + + + + + Dim ruleSetFile = CreateRuleSetFile(rulesetSource) + + additionalFlags = additionalFlags.Append("/ruleset:" + ruleSetFile.Path).ToArray() + End If + + Dim expectedErrorCount = If(rulesetSetToError, 1, 0) + Dim expectedWarningCount = If(Not rulesetSetToError, 1, 0) + Dim output = VerifyOutput(dir, file, + includeCurrentAssemblyAsAnalyzerReference:=False, + additionalFlags, + expectedErrorCount:=expectedErrorCount, + expectedWarningCount:=expectedWarningCount) + + Dim expectedOutput = If(rulesetSetToError, "error BC40008", "warning BC40008") + Assert.Contains(expectedOutput, output) + + Dim binaryPath As String = Path.Combine(dir.Path, "temp.dll") + Assert.True(IO.File.Exists(binaryPath) = Not rulesetSetToError) + + Dim pdbPath As String = Path.Combine(dir.Path, "temp.pdb") + Assert.True(IO.File.Exists(pdbPath) = Not rulesetSetToError) + + Dim docPath As String = Path.Combine(dir.Path, docName) + Assert.True(IO.File.Exists(docPath) = Not rulesetSetToError) + End Sub + + + + + + Public Sub AnalyzerWarnAsErrorDoesNotEmit(ByVal warnAsError As Boolean) + Dim source = " +Class C +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("temp.vb") + file.WriteAllText(source) + + Dim expectedErrorCount = If(warnAsError, 2, 0) + Dim expectedWarningCount = If(Not warnAsError, 2, 0) + Dim analyzer As DiagnosticAnalyzer = New WarningDiagnosticAnalyzer() ' Reports 2 warnings for each named type. + Dim additionalFlags = If(warnAsError, {"/warnaserror"}, Nothing) + Dim output = VerifyOutput(dir, file, + includeCurrentAssemblyAsAnalyzerReference:=False, + additionalFlags, + expectedErrorCount:=expectedErrorCount, + expectedWarningCount:=expectedWarningCount, + analyzers:=ImmutableArray.Create(analyzer)) + + Dim expectedODiagnosticSeverity = If(warnAsError, "error", "warning") + Assert.Contains($"{expectedODiagnosticSeverity} {WarningDiagnosticAnalyzer.Warning01.Id}", output) + Assert.Contains($"{expectedODiagnosticSeverity} {WarningDiagnosticAnalyzer.Warning03.Id}", output) + + Dim binaryPath As String = Path.Combine(dir.Path, "temp.dll") + Assert.True(IO.File.Exists(binaryPath) = Not warnAsError) + End Sub End Class -- GitLab