diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index 2e7c58608f48e..aae67c480e34d 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -2686,9 +2686,9 @@ internal override bool CompileMethods( filterOpt: filterOpt, cancellationToken: cancellationToken); - bool hasMethodBodyErrorOrWarningAsError = !FilterAndAppendAndFreeDiagnostics(diagnostics, ref methodBodyDiagnosticBag); + bool hasMethodBodyError = !FilterAndAppendAndFreeDiagnostics(diagnostics, ref methodBodyDiagnosticBag); - if (hasDeclarationErrors || hasMethodBodyErrorOrWarningAsError) + if (hasDeclarationErrors || hasMethodBodyError) { return false; } diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs index 78230daa47258..961ac64e1f5d5 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; -using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.MemoryMappedFiles; @@ -17,7 +16,6 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; -using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Diagnostics; @@ -9091,7 +9089,8 @@ private string VerifyOutput(TempDirectory sourceDir, TempFile sourceFile, string[] additionalFlags = null, int expectedInfoCount = 0, int expectedWarningCount = 0, - int expectedErrorCount = 0) + int expectedErrorCount = 0, + params DiagnosticAnalyzer[] analyzers) { var args = new[] { "/nologo", "/preferreduilang:en", "/t:library", @@ -9106,7 +9105,7 @@ private string VerifyOutput(TempDirectory sourceDir, TempFile sourceFile, args = args.Append(additionalFlags); } - var csc = CreateCSharpCompiler(null, sourceDir.Path, args); + var csc = CreateCSharpCompiler(null, sourceDir.Path, args, analyzers: analyzers.ToImmutableArrayOrEmpty()); var outWriter = new StringWriter(CultureInfo.InvariantCulture); var exitCode = csc.Run(outWriter); var output = outWriter.ToString(); @@ -10739,6 +10738,311 @@ public void InvalidPathCharacterInPdbPath() Assert.Equal(1, exitCode); Assert.Contains("error CS2021: File name 'test\\?.pdb' is empty, contains invalid characters, has a drive specification without an absolute path, or is too long", outWriter.ToString(), StringComparison.Ordinal); } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact(Skip = "https://github.com/dotnet/roslyn/issues/36215")] + public void TestSuppression_CompilerParserWarningAsError() + { + string source = @" +class C +{ + long M(int i) + { + // warning CS0078 : The 'l' suffix is easily confused with the digit '1' -- use 'L' for clarity + return 0l; + } +} +"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that parser warning CS0078 is reported. + var output = VerifyOutput(srcDirectory, srcFile, expectedWarningCount: 1, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("warning CS0078", output, StringComparison.Ordinal); + + // Verify that parser warning CS0078 is reported as error for /warnaserror. + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, + additionalFlags: new[] { "/warnAsError" }, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("error CS0078", output, StringComparison.Ordinal); + + // Verify that parser warning CS0078 is suppressed with diagnostic suppressor even with /warnaserror + // and info diagnostic is logged with programmatic suppression information. + var suppressor = new DiagnosticSuppressorForId("CS0078"); + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, expectedErrorCount: 0, + additionalFlags: new[] { "/warnAsError" }, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: suppressor); + Assert.DoesNotContain($"error CS0078", output, StringComparison.Ordinal); + Assert.DoesNotContain($"warning CS0078", output, StringComparison.Ordinal); + + // Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + var suppressionMessage = string.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.WRN_LowercaseEllSuffix, "l"), Location.None).GetMessage(CultureInfo.InvariantCulture), + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification); + Assert.Contains("info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact] + public void TestSuppression_CompilerSyntaxWarning() + { + // warning CS1522: Empty switch block + // NOTE: Empty switch block warning is reported by the C# language parser + string source = @" +class C +{ + void M(int i) + { + switch (i) + { + } + } +}"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that compiler warning CS1522 is reported. + var output = VerifyOutput(srcDirectory, srcFile, expectedWarningCount: 1, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("warning CS1522", output, StringComparison.Ordinal); + + // Verify that compiler warning CS1522 is suppressed with diagnostic suppressor + // and info diagnostic is logged with programmatic suppression information. + var suppressor = new DiagnosticSuppressorForId("CS1522"); + + // Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + var suppressionMessage = string.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.WRN_EmptySwitch), Location.None).GetMessage(CultureInfo.InvariantCulture), + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification); + + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: suppressor); + Assert.DoesNotContain($"warning CS1522", output, StringComparison.Ordinal); + Assert.Contains($"info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + // Verify that compiler warning CS1522 is reported as error for /warnaserror. + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, + additionalFlags: new[] { "/warnAsError" }, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("error CS1522", output, StringComparison.Ordinal); + + // Verify that compiler warning CS1522 is suppressed with diagnostic suppressor even with /warnaserror + // and info diagnostic is logged with programmatic suppression information. + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, expectedErrorCount: 0, + additionalFlags: new[] { "/warnAsError" }, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: suppressor); + Assert.DoesNotContain($"error CS1522", output, StringComparison.Ordinal); + Assert.DoesNotContain($"warning CS1522", output, StringComparison.Ordinal); + Assert.Contains("info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact] + public void TestSuppression_CompilerSemanticWarning() + { + string source = @" +class C +{ + // warning CS0169: The field 'C.f' is never used + private readonly int f; +}"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that compiler warning CS0169 is reported. + var output = VerifyOutput(srcDirectory, srcFile, expectedWarningCount: 1, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("warning CS0169", output, StringComparison.Ordinal); + + // Verify that compiler warning CS0169 is suppressed with diagnostic suppressor + // and info diagnostic is logged with programmatic suppression information. + var suppressor = new DiagnosticSuppressorForId("CS0169"); + + // Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + var suppressionMessage = string.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.WRN_UnreferencedField, "C.f"), Location.None).GetMessage(CultureInfo.InvariantCulture), + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification); + + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: suppressor); + Assert.DoesNotContain($"warning CS0169", output, StringComparison.Ordinal); + Assert.Contains("info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + // Verify that compiler warning CS0169 is reported as error for /warnaserror. + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, + additionalFlags: new[] { "/warnAsError" }, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("error CS0169", output, StringComparison.Ordinal); + + // Verify that compiler warning CS0169 is suppressed with diagnostic suppressor even with /warnaserror + // and info diagnostic is logged with programmatic suppression information. + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, expectedErrorCount: 0, + additionalFlags: new[] { "/warnAsError" }, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: suppressor); + Assert.DoesNotContain($"error CS0169", output, StringComparison.Ordinal); + Assert.DoesNotContain($"warning CS0169", output, StringComparison.Ordinal); + Assert.Contains("info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact] + public void TestNoSuppression_CompilerSyntaxError() + { + // error CS1001: Identifier expected + string source = @" +class { }"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that compiler syntax error CS1001 is reported. + var output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("error CS1001", output, StringComparison.Ordinal); + + // Verify that compiler syntax error CS1001 cannot be suppressed with diagnostic suppressor. + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: new DiagnosticSuppressorForId("CS1001")); + Assert.Contains("error CS1001", output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact] + public void TestNoSuppression_CompilerSemanticError() + { + // error CS0246: The type or namespace name 'UndefinedType' could not be found (are you missing a using directive or an assembly reference?) + string source = @" +class C +{ + void M(UndefinedType x) { } +}"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that compiler error CS0246 is reported. + var output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, includeCurrentAssemblyAsAnalyzerReference: false); + Assert.Contains("error CS0246", output, StringComparison.Ordinal); + + // Verify that compiler error CS0246 cannot be suppressed with diagnostic suppressor. + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: new DiagnosticSuppressorForId("CS0246")); + Assert.Contains("error CS0246", output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact] + public void TestSuppression_AnalyzerWarning() + { + string source = @" +class C { }"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that analyzer warning is reported. + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var output = VerifyOutput(srcDirectory, srcFile, expectedWarningCount: 1, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzer); + Assert.Contains($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + + // Verify that analyzer warning is suppressed with diagnostic suppressor + // and info diagnostic is logged with programmatic suppression information. + var suppressor = new DiagnosticSuppressorForId(analyzer.Descriptor.Id); + + // Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + var suppressionMessage = string.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + analyzer.Descriptor.MessageFormat, + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification); + + var analyzerAndSuppressor = new DiagnosticAnalyzer[] { analyzer, suppressor }; + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzerAndSuppressor); + Assert.DoesNotContain($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + Assert.Contains("info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + // Verify that analyzer warning is reported as error for /warnaserror. + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, + additionalFlags: new[] { "/warnAsError" }, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzer); + Assert.Contains($"error {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + + // Verify that analyzer warning is suppressed with diagnostic suppressor even with /warnaserror + // and info diagnostic is logged with programmatic suppression information. + output = VerifyOutput(srcDirectory, srcFile, expectedInfoCount: 1, expectedWarningCount: 0, + additionalFlags: new[] { "/warnAsError" }, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzerAndSuppressor); + Assert.DoesNotContain($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + Assert.Contains("info SP0001", output, StringComparison.Ordinal); + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal); + + // Verify that "NotConfigurable" analyzer warning cannot be suppressed with diagnostic suppressor. + analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: false); + suppressor = new DiagnosticSuppressorForId(analyzer.Descriptor.Id); + analyzerAndSuppressor = new DiagnosticAnalyzer[] { analyzer, suppressor }; + output = VerifyOutput(srcDirectory, srcFile, expectedWarningCount: 1, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzerAndSuppressor); + Assert.Contains($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } + + [WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + [Fact] + public void TestNoSuppression_AnalyzerError() + { + string source = @" +class C { }"; + var srcDirectory = Temp.CreateDirectory(); + var srcFile = srcDirectory.CreateFile("a.cs"); + srcFile.WriteAllText(source); + + // Verify that analyzer error is reported. + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Error, configurable: true); + var output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzer); + Assert.Contains($"error {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + + // Verify that analyzer error cannot be suppressed with diagnostic suppressor. + var suppressor = new DiagnosticSuppressorForId(analyzer.Descriptor.Id); + var analyzerAndSuppressor = new DiagnosticAnalyzer[] { analyzer, suppressor }; + output = VerifyOutput(srcDirectory, srcFile, expectedErrorCount: 1, + includeCurrentAssemblyAsAnalyzerReference: false, + analyzers: analyzerAndSuppressor); + Assert.Contains($"error {analyzer.Descriptor.Id}", output, StringComparison.Ordinal); + + CleanupAllGeneratedFiles(srcFile.Path); + } } [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] diff --git a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs index 32ac322577f41..aad9f88a019d8 100644 --- a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs @@ -2766,7 +2766,7 @@ public void TestSymbolStartAnalyzer_MultipleAnalyzers_NamedTypeAndMethods() } [Fact] - private void TestSymbolStartAnalyzer_MultipleAnalyzers_AllSymbolKinds() + public void TestSymbolStartAnalyzer_MultipleAnalyzers_AllSymbolKinds() { testCore("SymbolStartTopLevelRuleId", topLevel: true); testCore("SymbolStartRuleId", topLevel: false); diff --git a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticSuppressorTests.cs b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticSuppressorTests.cs new file mode 100644 index 0000000000000..89994af6b23cd --- /dev/null +++ b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticSuppressorTests.cs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +using static Microsoft.CodeAnalysis.CommonDiagnosticAnalyzers; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests +{ + public partial class DiagnosticSuppressorTests : CompilingTestBase + { + private static CSharpCompilation VerifyAnalyzerDiagnostics( + CSharpCompilation c, + DiagnosticAnalyzer[] analyzers, + params DiagnosticDescription[] expected) + => c.VerifyAnalyzerDiagnostics(analyzers, expected: expected); + + private static CSharpCompilation VerifySuppressedDiagnostics( + CSharpCompilation c, + DiagnosticAnalyzer[] analyzers, + params DiagnosticDescription[] expected) + => c.VerifySuppressedDiagnostics(analyzers, expected: expected); + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestSuppression_CompilerSyntaxWarning() + { + // NOTE: Empty switch block warning is reported by the C# language parser + string source = @" +class C +{ + void M(int i) + { + switch (i) + { + } + } +}"; + + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics( + // (7,9): warning CS1522: Empty switch block + // { + Diagnostic(ErrorCode.WRN_EmptySwitch, "{").WithLocation(7, 9)); + + // Verify compiler syntax warning can be suppressed with a suppressor. + var analyzers = new DiagnosticAnalyzer[] { new DiagnosticSuppressorForId("CS1522") }; + VerifySuppressedDiagnostics(compilation, analyzers, + // (7,9): warning CS1522: Empty switch block + // { + Diagnostic("CS1522", "{").WithLocation(7, 9)); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestSuppression_CompilerSemanticWarning() + { + string source = @" +class C +{ + // warning CS0169: The field 'C.f' is never used + private readonly int f; +}"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics( + // (5,26): warning CS0169: The field 'C.f' is never used + // private readonly int f; + Diagnostic(ErrorCode.WRN_UnreferencedField, "f").WithArguments("C.f").WithLocation(5, 26)); + + // Verify compiler semantic warning can be suppressed with a suppressor. + var analyzers = new DiagnosticAnalyzer[] { new DiagnosticSuppressorForId("CS0169") }; + VerifySuppressedDiagnostics(compilation, analyzers, + // (5,26): warning CS0169: The field 'C.f' is never used + // private readonly int f; + Diagnostic("CS0169", "f").WithArguments("C.f").WithLocation(5, 26)); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestNoSuppression_CompilerSyntaxError() + { + string source = @" +class { }"; + + var compilation = CreateCompilation(source); + + compilation.VerifyDiagnostics( + // (2,7): error CS1001: Identifier expected + // class { } + Diagnostic(ErrorCode.ERR_IdentifierExpected, "{").WithLocation(2, 7)); + + // Verify compiler syntax error cannot be suppressed. + var analyzers = new DiagnosticAnalyzer[] { new DiagnosticSuppressorForId("CS1001") }; + VerifySuppressedDiagnostics(compilation, analyzers); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestNoSuppression_CompilerSemanticError() + { + string source = @" +class C +{ + void M(UndefinedType x) { } +}"; + + var compilation = CreateCompilation(source); + + compilation.VerifyDiagnostics( + // (4,12): error CS0246: The type or namespace name 'UndefinedType' could not be found (are you missing a using directive or an assembly reference?) + // void M(UndefinedType x) { } + Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "UndefinedType").WithArguments("UndefinedType").WithLocation(4, 12)); + + // Verify compiler semantic error cannot be suppressed. + var analyzers = new DiagnosticAnalyzer[] { new DiagnosticSuppressorForId("CS0246") }; + VerifySuppressedDiagnostics(compilation, analyzers); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestSuppression_MultipleDiagnostics() + { + string source1 = @"class C1 { }"; + string source2 = @"class C2 { }"; + var compilation = CreateCompilation(new[] { source1, source2 }); + compilation.VerifyDiagnostics(); + + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var expectedDiagnostics = new DiagnosticDescription[] { + Diagnostic(analyzer.Descriptor.Id, source1), + Diagnostic(analyzer.Descriptor.Id, source2) + }; + VerifyAnalyzerDiagnostics(compilation, new DiagnosticAnalyzer[] { analyzer }, expectedDiagnostics); + + var analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, new DiagnosticSuppressorForId(analyzer.Descriptor.Id) }; + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors, expectedDiagnostics); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestSuppression_MultipleSuppressors_SameDiagnostic() + { + string source = @"class C1 { }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var expectedDiagnostic = Diagnostic(analyzer.Descriptor.Id, source); + VerifyAnalyzerDiagnostics(compilation, new DiagnosticAnalyzer[] { analyzer }, expectedDiagnostic); + + // Multiple suppressors with same suppression ID. + var analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, new DiagnosticSuppressorForId(analyzer.Descriptor.Id), new DiagnosticSuppressorForId(analyzer.Descriptor.Id) }; + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors, expectedDiagnostic); + + // Multiple suppressors with different suppression ID. + analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, new DiagnosticSuppressorForId(analyzer.Descriptor.Id, suppressionId: "SPR0001"), new DiagnosticSuppressorForId(analyzer.Descriptor.Id, suppressionId: "SPR0002") }; + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors, expectedDiagnostic); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestSuppression_MultipleSuppressors_DifferentDiagnostic() + { + string source = @"class C1 { private readonly int f; }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics( + // (1,33): warning CS0169: The field 'C1.f' is never used + // class C1 { private readonly int f; } + Diagnostic(ErrorCode.WRN_UnreferencedField, "f").WithArguments("C1.f").WithLocation(1, 33)); + + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + VerifyAnalyzerDiagnostics(compilation, new DiagnosticAnalyzer[] { analyzer }, + Diagnostic(analyzer.Descriptor.Id, source)); + + var suppressor1 = new DiagnosticSuppressorForId("CS0169"); + var suppresor2 = new DiagnosticSuppressorForId(analyzer.Descriptor.Id); + + var analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, suppressor1, suppresor2 }; + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors, + Diagnostic("CS0169", "f").WithArguments("C1.f").WithLocation(1, 33), + Diagnostic(analyzer.Descriptor.Id, source)); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestNoSuppression_SpecificOptionsTurnsOffSuppressor() + { + string source = @"class C1 { }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var expectedDiagnostic = Diagnostic(analyzer.Descriptor.Id, source); + VerifyAnalyzerDiagnostics(compilation, new DiagnosticAnalyzer[] { analyzer }, expectedDiagnostic); + + const string suppressionId = "SPR1001"; + var analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, new DiagnosticSuppressorForId(analyzer.Descriptor.Id, suppressionId) }; + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors, expectedDiagnostic); + + var specificDiagnosticOptions = compilation.Options.SpecificDiagnosticOptions.Add(suppressionId, ReportDiagnostic.Suppress); + compilation = compilation.WithOptions(compilation.Options.WithSpecificDiagnosticOptions(specificDiagnosticOptions)); + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestSuppression_AnalyzerDiagnostics_SeveritiesAndConfigurableMatrix() + { + string source = @" +class C { }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + var configurations = new[] { false, true }; + var severities = Enum.GetValues(typeof(DiagnosticSeverity)); + var originalSpecificDiagnosticOptions = compilation.Options.SpecificDiagnosticOptions; + + foreach (var configurable in configurations) + { + foreach (DiagnosticSeverity defaultSeverity in severities) + { + foreach (DiagnosticSeverity effectiveSeverity in severities) + { + var diagnostic = Diagnostic("ID1000", "class C { }") + .WithLocation(2, 1) + .WithDefaultSeverity(defaultSeverity) + .WithEffectiveSeverity(configurable ? effectiveSeverity : defaultSeverity); + + if (defaultSeverity == DiagnosticSeverity.Warning && + effectiveSeverity == DiagnosticSeverity.Error && + configurable) + { + diagnostic = diagnostic.WithWarningAsError(true); + } + + var analyzer = new CompilationAnalyzerWithSeverity(defaultSeverity, configurable); + var suppressor = new DiagnosticSuppressorForId(analyzer.Descriptor.Id); + var analyzersWithoutSuppressor = new DiagnosticAnalyzer[] { analyzer }; + var analyzersWithSuppressor = new DiagnosticAnalyzer[] { analyzer, suppressor }; + + var specificDiagnosticOptions = originalSpecificDiagnosticOptions.Add( + key: analyzer.Descriptor.Id, + value: DiagnosticDescriptor.MapSeverityToReport(effectiveSeverity)); + compilation = compilation.WithOptions(compilation.Options.WithSpecificDiagnosticOptions(specificDiagnosticOptions)); + + // Verify analyzer diagnostic without suppressor, also verify no suppressions. + VerifyAnalyzerDiagnostics(compilation, analyzersWithoutSuppressor, diagnostic); + VerifySuppressedDiagnostics(compilation, analyzersWithoutSuppressor); + + // Verify suppressed analyzer diagnostic, except when default severity is Error or diagnostic is not-configurable. + if (defaultSeverity == DiagnosticSeverity.Error || !configurable) + { + VerifySuppressedDiagnostics(compilation, analyzersWithSuppressor); + } + else + { + VerifySuppressedDiagnostics(compilation, analyzersWithSuppressor, diagnostic); + } + } + } + } + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestExceptionFromSuppressor() + { + string source = @" +class C { }"; + + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + testCore(new DiagnosticSuppressorThrowsExceptionFromSupportedSuppressions()); + testCore(new DiagnosticSuppressorThrowsExceptionFromReportedSuppressions(analyzer.Descriptor.Id)); + return; + + // Local functions. + void testCore(DiagnosticSuppressor suppressor) + { + var analyzersAndSuppresors = new DiagnosticAnalyzer[] { analyzer, suppressor }; + VerifyAnalyzerDiagnostics(compilation, analyzersAndSuppresors, + Diagnostic("AD0001").WithArguments(suppressor.ToString(), + typeof(NotImplementedException).FullName, + new NotImplementedException().Message) + .WithLocation(1, 1), + Diagnostic("ID1000", "class C { }").WithLocation(2, 1)); + + VerifySuppressedDiagnostics(compilation, analyzersAndSuppresors); + } + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestUnsupportedSuppressionReported() + { + string source = @" +class C { }"; + + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + const string supportedSuppressionId = "supportedId"; + const string unsupportedSuppressionId = "unsupportedId"; + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var suppressor = new DiagnosticSuppressor_UnsupportedSuppressionReported(analyzer.Descriptor.Id, supportedSuppressionId, unsupportedSuppressionId); + var analyzersAndSuppresors = new DiagnosticAnalyzer[] { analyzer, suppressor }; + + // "Reported suppression with ID '{0}' is not supported by the suppressor." + var exceptionMessage = string.Format(CodeAnalysisResources.UnsupportedSuppressionReported, unsupportedSuppressionId); + + VerifyAnalyzerDiagnostics(compilation, analyzersAndSuppresors, + Diagnostic("AD0001").WithArguments(suppressor.ToString(), + typeof(ArgumentException).FullName, + exceptionMessage) + .WithLocation(1, 1), + Diagnostic("ID1000", "class C { }").WithLocation(2, 1)); + + VerifySuppressedDiagnostics(compilation, analyzersAndSuppresors); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestInvalidDiagnosticSuppressionReported() + { + string source = @" +class C { }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + const string unsupportedSuppressedId = "UnsupportedId"; + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var suppressor = new DiagnosticSuppressor_InvalidDiagnosticSuppressionReported(analyzer.Descriptor.Id, unsupportedSuppressedId); + var analyzersAndSuppresors = new DiagnosticAnalyzer[] { analyzer, suppressor }; + + // "Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor." + var exceptionMessage = string.Format(CodeAnalysisResources.InvalidDiagnosticSuppressionReported, analyzer.Descriptor.Id, unsupportedSuppressedId); + + VerifyAnalyzerDiagnostics(compilation, analyzersAndSuppresors, + Diagnostic("AD0001").WithArguments(suppressor.ToString(), + typeof(System.ArgumentException).FullName, + exceptionMessage) + .WithLocation(1, 1), + Diagnostic("ID1000", "class C { }").WithLocation(2, 1)); + + VerifySuppressedDiagnostics(compilation, analyzersAndSuppresors); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestNonReportedDiagnosticCannotBeSuppressed() + { + string source = @" +class C { }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + const string nonReportedDiagnosticId = "NonReportedId"; + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var suppressor = new DiagnosticSuppressor_NonReportedDiagnosticCannotBeSuppressed(analyzer.Descriptor.Id, nonReportedDiagnosticId); + var analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, suppressor }; + + // "Non-reported diagnostic with ID '{0}' cannot be suppressed." + var exceptionMessage = string.Format(CodeAnalysisResources.NonReportedDiagnosticCannotBeSuppressed, nonReportedDiagnosticId); + + VerifyAnalyzerDiagnostics(compilation, analyzersAndSuppressors, + Diagnostic("AD0001").WithArguments(suppressor.ToString(), + typeof(System.ArgumentException).FullName, + exceptionMessage) + .WithLocation(1, 1), + Diagnostic("ID1000", "class C { }").WithLocation(2, 1)); + + VerifySuppressedDiagnostics(compilation, analyzersAndSuppressors); + } + + [Fact, WorkItem(20242, "https://github.com/dotnet/roslyn/issues/20242")] + public void TestProgrammaticSuppressionInfo_DiagnosticSuppressor() + { + string source = @"class C1 { }"; + var compilation = CreateCompilation(source); + compilation.VerifyDiagnostics(); + + var analyzer = new CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable: true); + var expectedDiagnostic = Diagnostic(analyzer.Descriptor.Id, source); + VerifyAnalyzerDiagnostics(compilation, new DiagnosticAnalyzer[] { analyzer }, expectedDiagnostic); + + const string suppressionId = "SPR1001"; + var suppressor = new DiagnosticSuppressorForId(analyzer.Descriptor.Id, suppressionId); + var analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, suppressor }; + var diagnostics = compilation.GetAnalyzerDiagnostics(analyzersAndSuppressors, reportSuppressedDiagnostics: true); + Assert.Single(diagnostics); + var suppressionInfo = diagnostics.Select(d => d.ProgrammaticSuppressionInfo).Single().Suppressions.Single(); + Assert.Equal(suppressionId, suppressionInfo.Id); + Assert.Equal(suppressor.SuppressionDescriptor.Justification, suppressionInfo.Justification); + + const string suppressionId2 = "SPR1002"; + var suppressor2 = new DiagnosticSuppressorForId(analyzer.Descriptor.Id, suppressionId2); + analyzersAndSuppressors = new DiagnosticAnalyzer[] { analyzer, suppressor, suppressor2 }; + diagnostics = compilation.GetAnalyzerDiagnostics(analyzersAndSuppressors, reportSuppressedDiagnostics: true); + Assert.Single(diagnostics); + var programmaticSuppression = diagnostics.Select(d => d.ProgrammaticSuppressionInfo).Single(); + Assert.Equal(2, programmaticSuppression.Suppressions.Count); + var orderedSuppressions = programmaticSuppression.Suppressions.Order().ToImmutableArrayOrEmpty(); + Assert.Equal(suppressionId, orderedSuppressions[0].Id); + Assert.Equal(suppressor.SuppressionDescriptor.Justification, orderedSuppressions[0].Justification); + Assert.Equal(suppressionId2, orderedSuppressions[1].Id); + Assert.Equal(suppressor2.SuppressionDescriptor.Justification, orderedSuppressions[1].Justification); + } + } +} diff --git a/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs b/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs index 278588e394aee..7d5cb21fbc42f 100644 --- a/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs +++ b/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs @@ -10,7 +10,6 @@ namespace Microsoft.CodeAnalysis { using System; - using System.Reflection; /// @@ -20,7 +19,7 @@ namespace Microsoft.CodeAnalysis { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CodeAnalysisResources { @@ -40,7 +39,7 @@ internal CodeAnalysisResources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CodeAnalysis.CodeAnalysisResources", typeof(CodeAnalysisResources).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CodeAnalysis.CodeAnalysisResources", typeof(CodeAnalysisResources).Assembly); resourceMan = temp; } return resourceMan; @@ -784,6 +783,15 @@ internal static string InvalidDiagnosticSpanReported { } } + /// + /// Looks up a localized string similar to Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor.. + /// + internal static string InvalidDiagnosticSuppressionReported { + get { + return ResourceManager.GetString("InvalidDiagnosticSuppressionReported", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid hash.. /// @@ -1063,6 +1071,15 @@ internal static string NodeOrTokenOutOfSequence { } } + /// + /// Looks up a localized string similar to Non-reported diagnostic with ID '{0}' cannot be suppressed.. + /// + internal static string NonReportedDiagnosticCannotBeSuppressed { + get { + return ResourceManager.GetString("NonReportedDiagnosticCannotBeSuppressed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Given operation has a non-null parent.. /// @@ -1414,6 +1431,42 @@ internal static string SupportedDiagnosticsHasNullDescriptor { } } + /// + /// Looks up a localized string similar to Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'.. + /// + internal static string SupportedSuppressionsHasNullDescriptor { + get { + return ResourceManager.GetString("SupportedSuppressionsHasNullDescriptor", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}'. + /// + internal static string SuppressionDiagnosticDescriptorMessage { + get { + return ResourceManager.GetString("SuppressionDiagnosticDescriptorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Programmatic suppression of an analyzer diagnostic. + /// + internal static string SuppressionDiagnosticDescriptorTitle { + get { + return ResourceManager.GetString("SuppressionDiagnosticDescriptorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space.. + /// + internal static string SuppressionIdCantBeNullOrWhitespace { + get { + return ResourceManager.GetString("SuppressionIdCantBeNullOrWhitespace", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows PDB writer doesn't support SourceLink feature: '{0}'. /// @@ -1594,6 +1647,15 @@ internal static string UnsupportedHashAlgorithm { } } + /// + /// Looks up a localized string similar to Reported suppression with ID '{0}' is not supported by the suppressor.. + /// + internal static string UnsupportedSuppressionReported { + get { + return ResourceManager.GetString("UnsupportedSuppressionReported", resourceCulture); + } + } + /// /// Looks up a localized string similar to Value too large to be represented as a 30 bit unsigned integer.. /// diff --git a/src/Compilers/Core/Portable/CodeAnalysisResources.resx b/src/Compilers/Core/Portable/CodeAnalysisResources.resx index 6c7d8ba8acf89..9328e25e04858 100644 --- a/src/Compilers/Core/Portable/CodeAnalysisResources.resx +++ b/src/Compilers/Core/Portable/CodeAnalysisResources.resx @@ -419,6 +419,9 @@ A DiagnosticDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + The rule set file has duplicate rules for '{0}' with differing actions '{1}' and '{2}'. @@ -443,6 +446,15 @@ Reported diagnostic with ID '{0}' is not supported by the analyzer. + + Reported suppression with ID '{0}' is not supported by the suppressor. + + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Reported diagnostic has an ID '{0}', which is not a valid identifier. @@ -452,6 +464,9 @@ Analyzer '{0}' contains a null descriptor in its 'SupportedDiagnostics'. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + The type '{0}' is not understood by the serialization binder. @@ -645,4 +660,10 @@ Invalid severity in analyzer config file. + + Programmatic suppression of an analyzer diagnostic + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + \ No newline at end of file diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.SuppressionDiagnostic.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.SuppressionDiagnostic.cs new file mode 100644 index 0000000000000..af84e3dce590f --- /dev/null +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.SuppressionDiagnostic.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis +{ + internal abstract partial class CommonCompiler + { + /// + /// Special informational diagnostic for each programmatic reported by a . + /// + private sealed class SuppressionDiagnostic : Diagnostic + { + private static readonly DiagnosticDescriptor s_suppressionDiagnosticDescriptor = new DiagnosticDescriptor( + "SP0001", + CodeAnalysisResources.SuppressionDiagnosticDescriptorTitle, + CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + "ProgrammaticSuppression", + DiagnosticSeverity.Info, + isEnabledByDefault: true); + + private readonly Diagnostic _originalDiagnostic; + private readonly string _suppressionId; + private readonly LocalizableString _suppressionJustification; + + public SuppressionDiagnostic( + Diagnostic originalDiagnostic, + string suppressionId, + LocalizableString suppressionJustification) + { + Debug.Assert(originalDiagnostic != null); + Debug.Assert(originalDiagnostic.ProgrammaticSuppressionInfo != null); + Debug.Assert(!string.IsNullOrEmpty(suppressionId)); + Debug.Assert(suppressionJustification != null); + + _originalDiagnostic = originalDiagnostic; + _suppressionId = suppressionId; + _suppressionJustification = suppressionJustification; + } + + public override DiagnosticDescriptor Descriptor => s_suppressionDiagnosticDescriptor; + + public override string Id => Descriptor.Id; + + public override string GetMessage(IFormatProvider formatProvider = null) + { + // Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + var localizableMessageFormat = s_suppressionDiagnosticDescriptor.MessageFormat.ToString(formatProvider); + return string.Format(formatProvider, + localizableMessageFormat, + _originalDiagnostic.Id, + _originalDiagnostic.GetMessage(formatProvider), + _suppressionId, + _suppressionJustification.ToString(formatProvider)); + } + + public override DiagnosticSeverity Severity => DiagnosticSeverity.Info; + public override bool IsSuppressed => false; + public override int WarningLevel => GetDefaultWarningLevel(DiagnosticSeverity.Info); + public override Location Location => _originalDiagnostic.Location; + public override IReadOnlyList AdditionalLocations => _originalDiagnostic.AdditionalLocations; + public override ImmutableDictionary Properties => ImmutableDictionary.Empty; + + public override bool Equals(Diagnostic obj) + { + var other = obj as SuppressionDiagnostic; + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(_originalDiagnostic, other._originalDiagnostic) && + Equals(_suppressionId, other._suppressionId) && + Equals(_suppressionJustification, other._suppressionJustification); + } + + public override bool Equals(object obj) + { + return this.Equals(obj as Diagnostic); + } + + public override int GetHashCode() + { + return Hash.Combine(_originalDiagnostic.GetHashCode(), + Hash.Combine(_suppressionId.GetHashCode(), _suppressionJustification.GetHashCode())); + } + + internal override Diagnostic WithLocation(Location location) + { + throw new NotSupportedException(); + } + + internal override Diagnostic WithSeverity(DiagnosticSeverity severity) + { + throw new NotSupportedException(); + } + + internal override Diagnostic WithIsSuppressed(bool isSuppressed) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index 65cd9834190ea..e8cad58b237d8 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -485,6 +485,14 @@ internal bool ReportDiagnostics(IEnumerable diagnostics, TextWriter { bool hasErrors = false; foreach (var diag in diagnostics) + { + reportDiagnostic(diag); + } + + return hasErrors; + + // Local functions + void reportDiagnostic(Diagnostic diag) { if (_reportedDiagnostics.Contains(diag)) { @@ -496,20 +504,38 @@ internal bool ReportDiagnostics(IEnumerable diagnostics, TextWriter // we previously created. //this assert isn't valid if we change the design to not bail out after each phase. //System.Diagnostics.Debug.Assert(diag.Severity != DiagnosticSeverity.Error); - continue; + return; } else if (diag.Severity == DiagnosticSeverity.Hidden) { // Not reported from the command-line compiler. - continue; + return; } // We want to report diagnostics with source suppression in the error log file. // However, these diagnostics should not be reported on the console output. errorLoggerOpt?.LogDiagnostic(diag); + + // If the diagnostic was suppressed by one or more DiagnosticSuppressor(s), then we report info diagnostics for each suppression + // so that the suppression information is available in the binary logs and verbose build logs. + if (diag.ProgrammaticSuppressionInfo != null) + { + foreach (var (id, justification) in diag.ProgrammaticSuppressionInfo.Suppressions) + { + var suppressionDiag = new SuppressionDiagnostic(diag, id, justification); + if (_reportedDiagnostics.Add(suppressionDiag)) + { + PrintError(suppressionDiag, consoleOutput); + } + } + + _reportedDiagnostics.Add(diag); + return; + } + if (diag.IsSuppressed) { - continue; + return; } // Diagnostics that aren't suppressed will be reported to the console output and, if they are errors, @@ -523,8 +549,6 @@ internal bool ReportDiagnostics(IEnumerable diagnostics, TextWriter _reportedDiagnostics.Add(diag); } - - return hasErrors; } /// Returns true if there were any errors, false otherwise. @@ -1022,9 +1046,16 @@ private void CompileAndEmit( // TryComplete, we may miss diagnostics. var hostDiagnostics = analyzerDriver.GetDiagnosticsAsync(compilation).Result; diagnostics.AddRange(hostDiagnostics); - if (hostDiagnostics.Any(IsReportedError)) + + if (!diagnostics.IsEmptyWithoutResolution) { - success = false; + // Apply diagnostic suppressions for analyzer and/or compiler diagnostics from diagnostic suppressors. + analyzerDriver.ApplyProgrammaticSuppressions(diagnostics, compilation); + + if (HasUnsuppressedErrors(diagnostics)) + { + success = false; + } } } } diff --git a/src/Compilers/Core/Portable/Compilation/Compilation.cs b/src/Compilers/Core/Portable/Compilation/Compilation.cs index cbf15d34923aa..63c6a97e491ad 100644 --- a/src/Compilers/Core/Portable/Compilation/Compilation.cs +++ b/src/Compilers/Core/Portable/Compilation/Compilation.cs @@ -1379,7 +1379,7 @@ internal bool FilterAndAppendDiagnostics(DiagnosticBag accumulator, IEnumerable< continue; } else if (filtered.Severity == DiagnosticSeverity.Error && - !filtered.IsSuppressed) + !filtered.IsSuppressed) { hasError = true; } diff --git a/src/Compilers/Core/Portable/Diagnostic/Diagnostic.DiagnosticWithProgrammaticSuppression.cs b/src/Compilers/Core/Portable/Diagnostic/Diagnostic.DiagnosticWithProgrammaticSuppression.cs new file mode 100644 index 0000000000000..4e7b584c32af6 --- /dev/null +++ b/src/Compilers/Core/Portable/Diagnostic/Diagnostic.DiagnosticWithProgrammaticSuppression.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis +{ + public abstract partial class Diagnostic + { + private sealed class DiagnosticWithProgrammaticSuppression : Diagnostic + { + private readonly Diagnostic _originalUnsuppressedDiagnostic; + private readonly ProgrammaticSuppressionInfo _programmaticSuppressionInfo; + + public DiagnosticWithProgrammaticSuppression( + Diagnostic originalUnsuppressedDiagnostic, + ProgrammaticSuppressionInfo programmaticSuppressionInfo) + { + Debug.Assert(!originalUnsuppressedDiagnostic.IsSuppressed); + Debug.Assert(originalUnsuppressedDiagnostic.ProgrammaticSuppressionInfo == null); + Debug.Assert(programmaticSuppressionInfo != null); + + _originalUnsuppressedDiagnostic = originalUnsuppressedDiagnostic; + _programmaticSuppressionInfo = programmaticSuppressionInfo; + } + + public override DiagnosticDescriptor Descriptor + { + get { return _originalUnsuppressedDiagnostic.Descriptor; } + } + + public override string Id + { + get { return Descriptor.Id; } + } + + public override string GetMessage(IFormatProvider formatProvider = null) + => _originalUnsuppressedDiagnostic.GetMessage(formatProvider); + + internal override IReadOnlyList Arguments + { + get { return _originalUnsuppressedDiagnostic.Arguments; } + } + + public override DiagnosticSeverity Severity + { + get { return _originalUnsuppressedDiagnostic.Severity; } + } + + public override bool IsSuppressed + { + get { return true; } + } + + internal override ProgrammaticSuppressionInfo ProgrammaticSuppressionInfo + { + get { return _programmaticSuppressionInfo; } + } + + public override int WarningLevel + { + get { return _originalUnsuppressedDiagnostic.WarningLevel; } + } + + public override Location Location + { + get { return _originalUnsuppressedDiagnostic.Location; } + } + + public override IReadOnlyList AdditionalLocations + { + get { return _originalUnsuppressedDiagnostic.AdditionalLocations; } + } + + public override ImmutableDictionary Properties + { + get { return _originalUnsuppressedDiagnostic.Properties; } + } + + public override bool Equals(Diagnostic obj) + { + var other = obj as DiagnosticWithProgrammaticSuppression; + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return Equals(_originalUnsuppressedDiagnostic, other._originalUnsuppressedDiagnostic) && + Equals(_programmaticSuppressionInfo, other._programmaticSuppressionInfo); + } + + public override bool Equals(object obj) + { + return this.Equals(obj as Diagnostic); + } + + public override int GetHashCode() + { + return Hash.Combine(_originalUnsuppressedDiagnostic.GetHashCode(), _programmaticSuppressionInfo.GetHashCode()); + } + + internal override Diagnostic WithLocation(Location location) + { + if (location == null) + { + throw new ArgumentNullException(nameof(location)); + } + + if (this.Location != location) + { + return new DiagnosticWithProgrammaticSuppression(_originalUnsuppressedDiagnostic.WithLocation(location), _programmaticSuppressionInfo); + } + + return this; + } + + internal override Diagnostic WithSeverity(DiagnosticSeverity severity) + { + if (this.Severity != severity) + { + return new DiagnosticWithProgrammaticSuppression(_originalUnsuppressedDiagnostic.WithSeverity(severity), _programmaticSuppressionInfo); + } + + return this; + } + + internal override Diagnostic WithIsSuppressed(bool isSuppressed) + { + // We do not support toggling suppressed diagnostic to unsuppressed. + if (!isSuppressed) + { + throw new ArgumentException(nameof(isSuppressed)); + } + + return this; + } + } + } +} diff --git a/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs b/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs index a62f7e812a167..b7d2b546f48d6 100644 --- a/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs +++ b/src/Compilers/Core/Portable/Diagnostic/Diagnostic.cs @@ -445,6 +445,19 @@ private string GetDebuggerDisplay() /// internal abstract Diagnostic WithIsSuppressed(bool isSuppressed); + /// + /// Create a new instance of this diagnostic with the given programmatic suppression info. + /// + internal Diagnostic WithProgrammaticSuppression(ProgrammaticSuppressionInfo programmaticSuppressionInfo) + { + Debug.Assert(this.ProgrammaticSuppressionInfo == null); + Debug.Assert(programmaticSuppressionInfo != null); + + return new DiagnosticWithProgrammaticSuppression(this, programmaticSuppressionInfo); + } + + internal virtual ProgrammaticSuppressionInfo ProgrammaticSuppressionInfo { get { return null; } } + // compatibility internal virtual int Code { get { return 0; } } diff --git a/src/Compilers/Core/Portable/Diagnostic/DiagnosticDescriptor.cs b/src/Compilers/Core/Portable/Diagnostic/DiagnosticDescriptor.cs index f5ec4d1bd323f..a40e4ddf7fa74 100644 --- a/src/Compilers/Core/Portable/Diagnostic/DiagnosticDescriptor.cs +++ b/src/Compilers/Core/Portable/Diagnostic/DiagnosticDescriptor.cs @@ -215,7 +215,8 @@ public ReportDiagnostic GetEffectiveSeverity(CompilationOptions compilationOptio return effectiveDiagnostic != null ? MapSeverityToReport(effectiveDiagnostic.Severity) : ReportDiagnostic.Suppress; } - private static ReportDiagnostic MapSeverityToReport(DiagnosticSeverity severity) + // internal for testing purposes. + internal static ReportDiagnostic MapSeverityToReport(DiagnosticSeverity severity) { switch (severity) { diff --git a/src/Compilers/Core/Portable/Diagnostic/ProgrammaticSuppressionInfo.cs b/src/Compilers/Core/Portable/Diagnostic/ProgrammaticSuppressionInfo.cs new file mode 100644 index 0000000000000..7374565d0d8d2 --- /dev/null +++ b/src/Compilers/Core/Portable/Diagnostic/ProgrammaticSuppressionInfo.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.Diagnostics +{ + /// + /// Contains information about the source of a programmatic diagnostic suppression produced by an . + /// + internal sealed class ProgrammaticSuppressionInfo : IEquatable + { + public ImmutableHashSet<(string Id, LocalizableString Justification)> Suppressions { get; } + + internal ProgrammaticSuppressionInfo(ImmutableHashSet<(string Id, LocalizableString Justification)> suppressions) + { + Suppressions = suppressions; + } + + public bool Equals(ProgrammaticSuppressionInfo other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + return other != null && + this.Suppressions.SetEquals(other.Suppressions); + } + + public override bool Equals(object obj) + { + return Equals(obj as ProgrammaticSuppressionInfo); + } + + public override int GetHashCode() + { + return Suppressions.Count; + } + } +} diff --git a/src/Compilers/Core/Portable/Diagnostic/SuppressionDescriptor.cs b/src/Compilers/Core/Portable/Diagnostic/SuppressionDescriptor.cs new file mode 100644 index 0000000000000..442545a0ca3e7 --- /dev/null +++ b/src/Compilers/Core/Portable/Diagnostic/SuppressionDescriptor.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis +{ + /// + /// Provides a description about a programmatic suppression of a by a . + /// + public sealed class SuppressionDescriptor : IEquatable + { + /// + /// An unique identifier for the suppression. + /// + public string Id { get; } + + /// + /// Identifier of the suppressed diagnostic, i.e. . + /// + public string SuppressedDiagnosticId { get; } + + /// + /// A localizable justification about the suppression. + /// + public LocalizableString Justification { get; } + + /// + /// Create a SuppressionDescriptor, which provides a justification about a programmatic suppression of a . + /// NOTE: For localizable , + /// use constructor overload . + /// + /// A unique identifier for the suppression. For example, suppression ID "SP1001". + /// Identifier of the suppressed diagnostic, i.e. . For example, compiler warning Id "CS0649". + /// Justification for the suppression. For example: "Suppress CS0649 on fields marked with YYY attribute as they are implicitly assigned.". + public SuppressionDescriptor( + string id, + string suppressedDiagnosticId, + string justification) + : this(id, suppressedDiagnosticId, (LocalizableString)justification) + { + } + + /// + /// Create a SuppressionDescriptor, which provides a localizable justification about a programmatic suppression of a . + /// + /// A unique identifier for the suppression. For example, suppression ID "SP1001". + /// Identifier of the suppressed diagnostic, i.e. . For example, compiler warning Id "CS0649". + /// Justification for the suppression. For example: "Suppress CS0649 on fields marked with YYY attribute as they are implicitly assigned.". + public SuppressionDescriptor( + string id, + string suppressedDiagnosticId, + LocalizableString justification) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException(CodeAnalysisResources.SuppressionIdCantBeNullOrWhitespace, nameof(id)); + } + + if (string.IsNullOrWhiteSpace(suppressedDiagnosticId)) + { + throw new ArgumentException(CodeAnalysisResources.DiagnosticIdCantBeNullOrWhitespace, nameof(suppressedDiagnosticId)); + } + + this.Id = id; + this.SuppressedDiagnosticId = suppressedDiagnosticId; + this.Justification = justification ?? throw new ArgumentNullException(nameof(justification)); + } + + public bool Equals(SuppressionDescriptor other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + return + other != null && + this.Id == other.Id && + this.SuppressedDiagnosticId == other.SuppressedDiagnosticId && + this.Justification.Equals(other.Justification); + } + + public override bool Equals(object obj) + { + return Equals(obj as SuppressionDescriptor); + } + + public override int GetHashCode() + { + return Hash.Combine(this.Id.GetHashCode(), + Hash.Combine(this.SuppressedDiagnosticId.GetHashCode(), this.Justification.GetHashCode())); + } + + /// + /// Returns a flag indicating if the suppression is disabled for the given . + /// + /// Compilation options + internal bool IsDisabled(CompilationOptions compilationOptions) + { + if (compilationOptions == null) + { + throw new ArgumentNullException(nameof(compilationOptions)); + } + + return compilationOptions.SpecificDiagnosticOptions.TryGetValue(Id, out var reportDiagnostic) && + reportDiagnostic == ReportDiagnostic.Suppress; + } + } +} diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs index aa44c95dcf54e..b27a9777240ab 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs @@ -73,16 +73,16 @@ internal ImmutableArray GetPendingAnalyzers(ImmutableArray getAnalyzerActionCounts, bool fullAnalysisResultForAnalyzersInScope) + internal void ApplySuppressionsAndStoreAnalysisResult(AnalysisScope analysisScope, AnalyzerDriver driver, Compilation compilation, Func getAnalyzerActionCounts, bool fullAnalysisResultForAnalyzersInScope) { Debug.Assert(!fullAnalysisResultForAnalyzersInScope || analysisScope.FilterTreeOpt == null, "Full analysis result cannot come from partial (tree) analysis."); foreach (var analyzer in analysisScope.Analyzers) { // Dequeue reported analyzer diagnostics from the driver and store them in our maps. - var syntaxDiagnostics = driver.DequeueLocalDiagnostics(analyzer, syntax: true, compilation: compilation); - var semanticDiagnostics = driver.DequeueLocalDiagnostics(analyzer, syntax: false, compilation: compilation); - var compilationDiagnostics = driver.DequeueNonLocalDiagnostics(analyzer, compilation); + var syntaxDiagnostics = driver.DequeueLocalDiagnosticsAndApplySuppressions(analyzer, syntax: true, compilation: compilation); + var semanticDiagnostics = driver.DequeueLocalDiagnosticsAndApplySuppressions(analyzer, syntax: false, compilation: compilation); + var compilationDiagnostics = driver.DequeueNonLocalDiagnosticsAndApplySuppressions(analyzer, compilation); lock (_gate) { @@ -348,8 +348,9 @@ private ImmutableDictionary GetTeleme foreach (var analyzer in analyzers) { var actionCounts = _analyzerActionCounts[analyzer]; + var suppressionActionCounts = analyzer is DiagnosticSuppressor ? 1 : 0; var executionTime = _analyzerExecutionTimeOpt != null ? _analyzerExecutionTimeOpt[analyzer] : default(TimeSpan); - var telemetryInfo = new AnalyzerTelemetryInfo(actionCounts, executionTime); + var telemetryInfo = new AnalyzerTelemetryInfo(actionCounts, suppressionActionCounts, executionTime); builder.Add(analyzer, telemetryInfo); } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs index 8f2e44f97b13f..e50e34d19612c 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs @@ -30,10 +30,21 @@ internal abstract partial class AnalyzerDriver : IDisposable private readonly Func _isGeneratedCode; + /// + /// Set of diagnostic suppressions that are suppressed via analyzer suppression actions. + /// + private readonly ConcurrentSet _programmaticSuppressions; + + /// + /// Flag indicating if the include any + /// which can suppress reported analyzer/compiler diagnostics. + /// + private readonly bool _hasDiagnosticSuppressors; + // Lazy fields/properties private CancellationTokenRegistration _queueRegistration; - protected ImmutableArray Analyzers { get; private set; } - protected AnalyzerManager AnalyzerManager { get; private set; } + protected ImmutableArray Analyzers { get; } + protected AnalyzerManager AnalyzerManager { get; } protected AnalyzerExecutor AnalyzerExecutor { get; private set; } protected CompilationData CurrentCompilationData { get; private set; } protected AnalyzerActions AnalyzerActions { get; private set; } @@ -155,6 +166,8 @@ protected AnalyzerDriver(ImmutableArray analyzers, AnalyzerM this.Analyzers = analyzers; this.AnalyzerManager = analyzerManager; _isGeneratedCode = (tree, ct) => GeneratedCodeUtilities.IsGeneratedCode(tree, isComment, ct); + _hasDiagnosticSuppressors = this.Analyzers.Any(a => a is DiagnosticSuppressor); + _programmaticSuppressions = _hasDiagnosticSuppressors ? new ConcurrentSet() : null; } /// @@ -272,7 +285,8 @@ internal void Initialize( var analyzerExecutor = AnalyzerExecutor.Create( compilation, analysisOptions.Options ?? AnalyzerOptions.Empty, addNotCategorizedDiagnosticOpt, newOnAnalyzerException, analysisOptions.AnalyzerExceptionFilter, IsCompilerAnalyzer, AnalyzerManager, ShouldSkipAnalysisOnGeneratedCode, ShouldSuppressGeneratedCodeDiagnostic, IsGeneratedOrHiddenCodeLocation, GetAnalyzerGate, - analysisOptions.LogAnalyzerExecutionTime, addCategorizedLocalDiagnosticOpt, addCategorizedNonLocalDiagnosticOpt, cancellationToken); + getSemanticModel: tree => CurrentCompilationData.GetOrCreateCachedSemanticModel(tree, compilation, cancellationToken), + analysisOptions.LogAnalyzerExecutionTime, addCategorizedLocalDiagnosticOpt, addCategorizedNonLocalDiagnosticOpt, s => _programmaticSuppressions.Add(s), cancellationToken); Initialize(analyzerExecutor, diagnosticQueue, compilationData, cancellationToken); } @@ -588,19 +602,156 @@ public async Task> GetDiagnosticsAsync(Compilation co return allDiagnostics.ToReadOnlyAndFree(); } - public ImmutableArray DequeueLocalDiagnostics(DiagnosticAnalyzer analyzer, bool syntax, Compilation compilation) + public void ApplyProgrammaticSuppressions(DiagnosticBag reportedDiagnostics, Compilation compilation) + { + Debug.Assert(!reportedDiagnostics.IsEmptyWithoutResolution); + if (!_hasDiagnosticSuppressors) + { + return; + } + + var newDiagnostics = ApplyProgrammaticSuppressionsCore(reportedDiagnostics.ToReadOnly(), compilation); + reportedDiagnostics.Clear(); + reportedDiagnostics.AddRange(newDiagnostics); + } + + public ImmutableArray ApplyProgrammaticSuppressions(ImmutableArray reportedDiagnostics, Compilation compilation) + { + if (reportedDiagnostics.IsEmpty || + !_hasDiagnosticSuppressors) + { + return reportedDiagnostics; + } + + return ApplyProgrammaticSuppressionsCore(reportedDiagnostics, compilation); + } + + private ImmutableArray ApplyProgrammaticSuppressionsCore(ImmutableArray reportedDiagnostics, Compilation compilation) + { + Debug.Assert(_hasDiagnosticSuppressors); + Debug.Assert(!reportedDiagnostics.IsEmpty); + Debug.Assert(_programmaticSuppressions != 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()) + { + return reportedDiagnostics; + } + + executeSuppressionActions(suppressableDiagnostics, concurrent: compilation.Options.ConcurrentBuild); + if (_programmaticSuppressions.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)) + { + Debug.Assert(suppressableDiagnostics.Contains(diagnostic)); + Debug.Assert(!diagnostic.IsSuppressed); + builder.Add(diagnostic.WithProgrammaticSuppression(programmaticSuppressionInfo)); + } + else + { + builder.Add(diagnostic); + } + } + + return builder.ToImmutableAndFree(); + + void executeSuppressionActions(IEnumerable reportedDiagnostics, bool concurrent) + { + var suppressors = this.Analyzers.OfType(); + if (concurrent) + { + Parallel.ForEach(suppressors, suppressor => + { + AnalyzerExecutor.ExecuteSuppressionAction(suppressor, getSuppressableDiagnostics(suppressor)); + }); + } + else + { + foreach (var suppressor in suppressors) + { + AnalyzerExecutor.ExecuteSuppressionAction(suppressor, getSuppressableDiagnostics(suppressor)); + } + } + + return; + + ImmutableArray getSuppressableDiagnostics(DiagnosticSuppressor suppressor) + { + var supportedSuppressions = AnalyzerManager.GetSupportedSuppressionDescriptors(suppressor, AnalyzerExecutor); + if (supportedSuppressions.IsEmpty) + { + return ImmutableArray.Empty; + } + + var builder = ArrayBuilder.GetInstance(); + foreach (var diagnostic in reportedDiagnostics) + { + if (supportedSuppressions.Contains(s => s.SuppressedDiagnosticId == diagnostic.Id)) + { + builder.Add(diagnostic); + } + } + + return builder.ToImmutableAndFree(); + } + } + + static ImmutableDictionary createProgrammaticSuppressionsByDiagnosticMap(ConcurrentSet programmaticSuppressions) + { + var programmaticSuppressionsBuilder = PooledDictionary.Builder>.GetInstance(); + foreach (var programmaticSuppression in programmaticSuppressions) + { + if (!programmaticSuppressionsBuilder.TryGetValue(programmaticSuppression.SuppressedDiagnostic, out var set)) + { + set = ImmutableHashSet.CreateBuilder<(string, LocalizableString)>(); + programmaticSuppressionsBuilder.Add(programmaticSuppression.SuppressedDiagnostic, set); + } + + set.Add((programmaticSuppression.Descriptor.Id, programmaticSuppression.Descriptor.Justification)); + } + + var mapBuilder = ImmutableDictionary.CreateBuilder(); + foreach (var (diagnostic, set) in programmaticSuppressionsBuilder) + { + mapBuilder.Add(diagnostic, new ProgrammaticSuppressionInfo(set.ToImmutable())); + } + + return mapBuilder.ToImmutable(); + } + } + + public ImmutableArray DequeueLocalDiagnosticsAndApplySuppressions(DiagnosticAnalyzer analyzer, bool syntax, Compilation compilation) { var diagnostics = syntax ? DiagnosticQueue.DequeueLocalSyntaxDiagnostics(analyzer) : DiagnosticQueue.DequeueLocalSemanticDiagnostics(analyzer); - return FilterDiagnosticsSuppressedInSource(diagnostics, compilation, CurrentCompilationData.SuppressMessageAttributeState); + return FilterDiagnosticsSuppressedInSourceOrByAnalyzers(diagnostics, compilation); } - public ImmutableArray DequeueNonLocalDiagnostics(DiagnosticAnalyzer analyzer, Compilation compilation) + public ImmutableArray DequeueNonLocalDiagnosticsAndApplySuppressions(DiagnosticAnalyzer analyzer, Compilation compilation) { var diagnostics = DiagnosticQueue.DequeueNonLocalDiagnostics(analyzer); - return FilterDiagnosticsSuppressedInSource(diagnostics, compilation, CurrentCompilationData.SuppressMessageAttributeState); + return FilterDiagnosticsSuppressedInSourceOrByAnalyzers(diagnostics, compilation); + } + + private ImmutableArray FilterDiagnosticsSuppressedInSourceOrByAnalyzers(ImmutableArray diagnostics, Compilation compilation) + { + diagnostics = FilterDiagnosticsSuppressedInSource(diagnostics, compilation, CurrentCompilationData.SuppressMessageAttributeState); + return ApplyProgrammaticSuppressions(diagnostics, compilation); } - private ImmutableArray FilterDiagnosticsSuppressedInSource(ImmutableArray diagnostics, Compilation compilation, SuppressMessageAttributeState suppressMessageState) + private static ImmutableArray FilterDiagnosticsSuppressedInSource(ImmutableArray diagnostics, Compilation compilation, SuppressMessageAttributeState suppressMessageState) { if (diagnostics.IsEmpty) { diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs index 393e4a6dbfa07..41f1fab843bfa 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerExecutor.cs @@ -36,11 +36,13 @@ internal class AnalyzerExecutor private readonly Action _addNonCategorizedDiagnosticOpt; private readonly Action _addCategorizedLocalDiagnosticOpt; private readonly Action _addCategorizedNonLocalDiagnosticOpt; + private readonly Action _addSuppressionOpt; private readonly Action _onAnalyzerException; private readonly Func _analyzerExceptionFilter; private readonly AnalyzerManager _analyzerManager; private readonly Func _isCompilerAnalyzer; private readonly Func _getAnalyzerGateOpt; + private readonly Func _getSemanticModelOpt; private readonly Func _shouldSkipAnalysisOnGeneratedCode; private readonly Func _shouldSuppressGeneratedCodeDiagnostic; private readonly Func _isGeneratedCodeLocation; @@ -75,12 +77,14 @@ internal class AnalyzerExecutor /// It should return a unique gate object for the given analyzer instance for non-concurrent analyzers, and null otherwise. /// All analyzer callbacks for non-concurrent analyzers will be guarded with a lock on the gate. /// + /// Delegate to get a semantic model for the given syntax tree which can be shared across analyzers. /// Delegate to identify if analysis should be skipped on generated code. /// Delegate to identify if diagnostic reported while analyzing generated code should be suppressed. /// Delegate to identify if the given location is in generated code. /// Flag indicating whether we need to log analyzer execution time. /// Optional delegate to add categorized local analyzer diagnostics. /// Optional delegate to add categorized non-local analyzer diagnostics. + /// Optional thread-safe delegate to add diagnostic suppressions from suppressors. /// Cancellation token. public static AnalyzerExecutor Create( Compilation compilation, @@ -94,9 +98,11 @@ public static AnalyzerExecutor Create( Func shouldSuppressGeneratedCodeDiagnostic, Func isGeneratedCodeLocation, Func getAnalyzerGate, + Func getSemanticModel, bool logExecutionTime = false, Action addCategorizedLocalDiagnosticOpt = null, Action addCategorizedNonLocalDiagnosticOpt = null, + Action addSuppressionOpt = null, CancellationToken cancellationToken = default(CancellationToken)) { // We can either report categorized (local/non-local) diagnostics or non-categorized diagnostics. @@ -107,7 +113,8 @@ public static AnalyzerExecutor Create( return new AnalyzerExecutor(compilation, analyzerOptions, addNonCategorizedDiagnosticOpt, onAnalyzerException, analyzerExceptionFilter, isCompilerAnalyzer, analyzerManager, shouldSkipAnalysisOnGeneratedCode, shouldSuppressGeneratedCodeDiagnostic, isGeneratedCodeLocation, - getAnalyzerGate, analyzerExecutionTimeMapOpt, addCategorizedLocalDiagnosticOpt, addCategorizedNonLocalDiagnosticOpt, cancellationToken); + getAnalyzerGate, getSemanticModel, analyzerExecutionTimeMapOpt, addCategorizedLocalDiagnosticOpt, addCategorizedNonLocalDiagnosticOpt, + addSuppressionOpt, cancellationToken); } /// @@ -133,12 +140,14 @@ public static AnalyzerExecutor CreateForSupportedDiagnostics( shouldSuppressGeneratedCodeDiagnostic: (diagnostic, analyzer, compilation, ct) => false, isGeneratedCodeLocation: (_1, _2) => false, getAnalyzerGateOpt: null, + getSemanticModelOpt: null, onAnalyzerException: onAnalyzerException, analyzerExceptionFilter: null, analyzerManager: analyzerManager, analyzerExecutionTimeMapOpt: null, addCategorizedLocalDiagnosticOpt: null, addCategorizedNonLocalDiagnosticOpt: null, + addSuppressionOpt: null, cancellationToken: cancellationToken); } @@ -154,9 +163,11 @@ private AnalyzerExecutor( Func shouldSuppressGeneratedCodeDiagnostic, Func isGeneratedCodeLocation, Func getAnalyzerGateOpt, + Func getSemanticModelOpt, ConcurrentDictionary> analyzerExecutionTimeMapOpt, Action addCategorizedLocalDiagnosticOpt, Action addCategorizedNonLocalDiagnosticOpt, + Action addSuppressionOpt, CancellationToken cancellationToken) { _compilation = compilation; @@ -170,9 +181,11 @@ private AnalyzerExecutor( _shouldSuppressGeneratedCodeDiagnostic = shouldSuppressGeneratedCodeDiagnostic; _isGeneratedCodeLocation = isGeneratedCodeLocation; _getAnalyzerGateOpt = getAnalyzerGateOpt; + _getSemanticModelOpt = getSemanticModelOpt; _analyzerExecutionTimeMapOpt = analyzerExecutionTimeMapOpt; _addCategorizedLocalDiagnosticOpt = addCategorizedLocalDiagnosticOpt; _addCategorizedNonLocalDiagnosticOpt = addCategorizedNonLocalDiagnosticOpt; + _addSuppressionOpt = addSuppressionOpt; _cancellationToken = cancellationToken; _compilationAnalysisValueProviderFactory = new CompilationAnalysisValueProviderFactory(); @@ -187,7 +200,8 @@ public AnalyzerExecutor WithCancellationToken(CancellationToken cancellationToke return new AnalyzerExecutor(_compilation, _analyzerOptions, _addNonCategorizedDiagnosticOpt, _onAnalyzerException, _analyzerExceptionFilter, _isCompilerAnalyzer, _analyzerManager, _shouldSkipAnalysisOnGeneratedCode, _shouldSuppressGeneratedCodeDiagnostic, _isGeneratedCodeLocation, - _getAnalyzerGateOpt, _analyzerExecutionTimeMapOpt, _addCategorizedLocalDiagnosticOpt, _addCategorizedNonLocalDiagnosticOpt, cancellationToken); + _getAnalyzerGateOpt, _getSemanticModelOpt, _analyzerExecutionTimeMapOpt, _addCategorizedLocalDiagnosticOpt, _addCategorizedNonLocalDiagnosticOpt, + _addSuppressionOpt, cancellationToken); } internal Compilation Compilation => _compilation; @@ -266,6 +280,35 @@ public void ExecuteSymbolStartActions( } } + /// + /// Executes the given diagnostic suppressor. + /// + /// Suppressor to be executed. + /// Reported analyzer/compiler diagnostics that can be suppressed. + public void ExecuteSuppressionAction(DiagnosticSuppressor suppressor, ImmutableArray reportedDiagnostics) + { + Debug.Assert(_addSuppressionOpt != null); + + if (reportedDiagnostics.IsEmpty) + { + return; + } + + _cancellationToken.ThrowIfCancellationRequested(); + + var supportedSuppressions = _analyzerManager.GetSupportedSuppressionDescriptors(suppressor, this); + Func isSupportedSuppression = supportedSuppressions.Contains; + Action action = suppressor.ReportSuppressions; + var context = new SuppressionAnalysisContext(_compilation, _analyzerOptions, + reportedDiagnostics, _addSuppressionOpt, isSupportedSuppression, _getSemanticModelOpt, _cancellationToken); + + ExecuteAndCatchIfThrows( + suppressor, + data => data.action(data.context), + (action, context), + new AnalysisContextInfo(_compilation)); + } + /// /// Tries to executes compilation actions or compilation end actions. /// diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.AnalyzerExecutionContext.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.AnalyzerExecutionContext.cs index 68a45a5ed99ee..09afb49e84fdc 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.AnalyzerExecutionContext.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.AnalyzerExecutionContext.cs @@ -48,9 +48,14 @@ public AnalyzerExecutionContext(DiagnosticAnalyzer analyzer) private Dictionary> _lazySymbolScopeTasks; /// - /// Supported descriptors for diagnostic analyzer. + /// Supported diagnostic descriptors for diagnostic analyzer, if any. /// - private ImmutableArray _lazyDescriptors = default(ImmutableArray); + private ImmutableArray _lazyDiagnosticDescriptors = default(ImmutableArray); + + /// + /// Supported suppression descriptors for diagnostic suppressor, if any. + /// + private ImmutableArray _lazySuppressionDescriptors = default(ImmutableArray); public Task GetSessionAnalysisScopeTask(AnalyzerExecutor analyzerExecutor) { @@ -217,23 +222,36 @@ public void ClearSymbolScopeTask(ISymbol symbol) } } - public ImmutableArray GetOrComputeDescriptors(DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor) + public ImmutableArray GetOrComputeDiagnosticDescriptors(DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor) + => GetOrComputeDescriptors(ref _lazyDiagnosticDescriptors, ComputeDiagnosticDescriptors, _gate, analyzer, analyzerExecutor); + + public ImmutableArray GetOrComputeSuppressionDescriptors(DiagnosticSuppressor suppressor, AnalyzerExecutor analyzerExecutor) + => GetOrComputeDescriptors(ref _lazySuppressionDescriptors, ComputeSuppressionDescriptors, _gate, suppressor, analyzerExecutor); + + private static ImmutableArray GetOrComputeDescriptors( + ref ImmutableArray lazyDescriptors, + Func> computeDescriptors, + object gate, + DiagnosticAnalyzer analyzer, + AnalyzerExecutor analyzerExecutor) { - if (!_lazyDescriptors.IsDefault) + if (!lazyDescriptors.IsDefault) { - return _lazyDescriptors; + return lazyDescriptors; } // Otherwise, compute the value. - var descriptors = ComputeDescriptors(analyzer, analyzerExecutor); - ImmutableInterlocked.InterlockedInitialize(ref _lazyDescriptors, descriptors); - return _lazyDescriptors; + // We do so outside the lock statement as we are calling into user code, which may be a long running operation. + var descriptors = computeDescriptors(analyzer, analyzerExecutor); + + ImmutableInterlocked.InterlockedInitialize(ref lazyDescriptors, descriptors); + return lazyDescriptors; } /// /// Compute and exception handler for the given . /// - private static ImmutableArray ComputeDescriptors( + private static ImmutableArray ComputeDiagnosticDescriptors( DiagnosticAnalyzer analyzer, AnalyzerExecutor analyzerExecutor) { @@ -282,6 +300,40 @@ private static ImmutableArray ComputeDescriptors( return supportedDiagnostics; } + private static ImmutableArray ComputeSuppressionDescriptors( + DiagnosticAnalyzer analyzer, + AnalyzerExecutor analyzerExecutor) + { + var descriptors = ImmutableArray.Empty; + + if (analyzer is DiagnosticSuppressor suppressor) + { + // Catch Exception from suppressor.SupportedSuppressions + analyzerExecutor.ExecuteAndCatchIfThrows( + analyzer, + _ => + { + var descriptorsLocal = suppressor.SupportedSuppressions; + if (!descriptorsLocal.IsDefaultOrEmpty) + { + foreach (var descriptor in descriptorsLocal) + { + if (descriptor == null) + { + // Disallow null descriptors. + throw new ArgumentException(string.Format(CodeAnalysisResources.SupportedSuppressionsHasNullDescriptor, analyzer.ToString()), nameof(DiagnosticSuppressor.SupportedSuppressions)); + } + } + + descriptors = descriptorsLocal; + } + }, + argument: default(object)); + } + + return descriptors; + } + public bool TryProcessCompletedMemberAndGetPendingSymbolEndActionsForContainer( ISymbol containingSymbol, ISymbol processedMemberSymbol, diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.cs index 333328ea32bee..4b98724ab6a73 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerManager.cs @@ -235,7 +235,18 @@ public ImmutableArray GetSupportedDiagnosticDescriptors( AnalyzerExecutor analyzerExecutor) { var analyzerExecutionContext = GetAnalyzerExecutionContext(analyzer); - return analyzerExecutionContext.GetOrComputeDescriptors(analyzer, analyzerExecutor); + return analyzerExecutionContext.GetOrComputeDiagnosticDescriptors(analyzer, analyzerExecutor); + } + + /// + /// Return of given . + /// + public ImmutableArray GetSupportedSuppressionDescriptors( + DiagnosticSuppressor suppressor, + AnalyzerExecutor analyzerExecutor) + { + var analyzerExecutionContext = GetAnalyzerExecutionContext(suppressor); + return analyzerExecutionContext.GetOrComputeSuppressionDescriptors(suppressor, analyzerExecutor); } internal bool IsSupportedDiagnostic(DiagnosticAnalyzer analyzer, Diagnostic diagnostic, Func isCompilerAnalyzer, AnalyzerExecutor analyzerExecutor) @@ -309,6 +320,17 @@ internal bool IsDiagnosticAnalyzerSuppressed( } } + if (analyzer is DiagnosticSuppressor suppressor) + { + foreach (var suppressionDescriptor in GetSupportedSuppressionDescriptors(suppressor, analyzerExecutor)) + { + if (!suppressionDescriptor.IsDisabled(options)) + { + return false; + } + } + } + return true; } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerTelemetry.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerTelemetry.cs index 008b4e8c7d8b8..d3ba86671f646 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerTelemetry.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerTelemetry.cs @@ -89,6 +89,13 @@ public sealed class AnalyzerTelemetryInfo /// public int OperationBlockActionsCount { get; set; } = 0; + /// + /// Count of registered suppression actions. + /// This is the same as count of s as each suppressor + /// has a single suppression action, i.e. . + /// + public int SuppressionActionsCount { get; set; } = 0; + /// /// Total execution time. /// @@ -99,7 +106,7 @@ public sealed class AnalyzerTelemetryInfo /// public bool Concurrent { get; set; } - internal AnalyzerTelemetryInfo(AnalyzerActionCounts actionCounts, TimeSpan executionTime) + internal AnalyzerTelemetryInfo(AnalyzerActionCounts actionCounts, int suppressionActionCounts, TimeSpan executionTime) { CompilationStartActionsCount = actionCounts.CompilationStartActionsCount; CompilationEndActionsCount = actionCounts.CompilationEndActionsCount; @@ -121,6 +128,8 @@ internal AnalyzerTelemetryInfo(AnalyzerActionCounts actionCounts, TimeSpan execu OperationBlockEndActionsCount = actionCounts.OperationBlockEndActionsCount; OperationBlockActionsCount = actionCounts.OperationBlockActionsCount; + SuppressionActionsCount = suppressionActionCounts; + ExecutionTime = executionTime; Concurrent = actionCounts.Concurrent; } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs index ac74d0e36231f..7c409ee82d3d5 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/CompilationWithAnalyzers.cs @@ -420,7 +420,7 @@ private async Task ComputeAnalyzerDiagnosticsWithoutStateTrackingAsync(Cancellat } Func getAnalyzerActionCounts = analyzer => analyzerActionCounts[analyzer]; - _analysisResultBuilder.StoreAnalysisResult(analysisScope, driver, compilation, getAnalyzerActionCounts, fullAnalysisResultForAnalyzersInScope: true); + _analysisResultBuilder.ApplySuppressionsAndStoreAnalysisResult(analysisScope, driver, compilation, getAnalyzerActionCounts, fullAnalysisResultForAnalyzersInScope: true); } finally { @@ -450,7 +450,8 @@ private async Task> GetAllDiagnosticsWithoutStateTrac // Force compilation diagnostics and wait for analyzer execution to complete. var compDiags = compilation.GetDiagnostics(cancellationToken); var analyzerDiags = await driver.GetDiagnosticsAsync(compilation).ConfigureAwait(false); - return compDiags.AddRange(analyzerDiags); + var reportedDiagnostics = compDiags.AddRange(analyzerDiags); + return driver.ApplyProgrammaticSuppressions(reportedDiagnostics, compilation); } finally { @@ -898,7 +899,7 @@ private async Task ComputeAnalyzerDiagnosticsCoreAsync(AnalyzerDriver driver, As finally { // Update the diagnostic results based on the diagnostics reported on the driver. - _analysisResultBuilder.StoreAnalysisResult(analysisScope, driver, _compilation, _analysisState.GetAnalyzerActionCounts, fullAnalysisResultForAnalyzersInScope: false); + _analysisResultBuilder.ApplySuppressionsAndStoreAnalysisResult(analysisScope, driver, _compilation, _analysisState.GetAnalyzerActionCounts, fullAnalysisResultForAnalyzersInScope: false); } } } @@ -1227,8 +1228,9 @@ public async Task GetAnalyzerTelemetryInfoAsync(Diagnosti try { var actionCounts = await GetAnalyzerActionCountsAsync(analyzer, cancellationToken).ConfigureAwait(false); + var suppressionActionCounts = analyzer is DiagnosticSuppressor ? 1 : 0; var executionTime = GetAnalyzerExecutionTime(analyzer); - return new AnalyzerTelemetryInfo(actionCounts, executionTime); + return new AnalyzerTelemetryInfo(actionCounts, suppressionActionCounts, executionTime); } catch (Exception e) when (FatalError.ReportUnlessCanceled(e)) { diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContext.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContext.cs index 5156e3f10c24a..c27d37a10ad94 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContext.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContext.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Immutable; +using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.FlowAnalysis; using Microsoft.CodeAnalysis.Operations; @@ -1453,4 +1454,92 @@ public void ReportDiagnostic(Diagnostic diagnostic) /// public ControlFlowGraph GetControlFlowGraph() => DiagnosticAnalysisContextHelpers.GetControlFlowGraph(Operation, _getControlFlowGraphOpt, _cancellationToken); } + + /// + /// Context for suppressing analyzer and/or compiler non-error diagnostics reported for the compilation. + /// + public struct SuppressionAnalysisContext + { + private readonly Action _addSuppression; + private readonly Func _isSupportedSuppressionDescriptor; + private readonly Func _getSemanticModel; + + /// + /// Analyzer and/or compiler non-error diagnostics reported for the compilation. + /// Each only receives diagnostics whose IDs were declared suppressible in its . + /// This may be a subset of the full set of reported diagnostics, as an optimization for + /// supporting incremental and partial analysis scenarios. + /// A diagnostic is considered suppressible by a DiagnosticSuppressor if *all* of the following conditions are met: + /// 1. Diagnostic is not already suppressed in source via pragma/suppress message attribute. + /// 2. Diagnostic's is not . + /// 3. Diagnostic is not tagged with custom tag. + /// + public ImmutableArray ReportedDiagnostics { get; } + + /// + /// for the context. + /// + public Compilation Compilation { get; } + + /// + /// Options specified for the analysis. + /// + public AnalyzerOptions Options { get; } + + /// + /// Token to check for requested cancellation of the analysis. + /// + public CancellationToken CancellationToken { get; } + + internal SuppressionAnalysisContext( + Compilation compilation, + AnalyzerOptions options, + ImmutableArray reportedDiagnostics, + Action suppressDiagnostic, + Func isSupportedSuppressionDescriptor, + Func getSemanticModel, + CancellationToken cancellationToken) + { + Compilation = compilation; + Options = options; + ReportedDiagnostics = reportedDiagnostics; + _addSuppression = suppressDiagnostic; + _isSupportedSuppressionDescriptor = isSupportedSuppressionDescriptor; + _getSemanticModel = getSemanticModel; + CancellationToken = cancellationToken; + } + + /// + /// Report a for a reported diagnostic. + /// + public void ReportSuppression(Suppression suppression) + { + if (!ReportedDiagnostics.Contains(suppression.SuppressedDiagnostic)) + { + // Non-reported diagnostic with ID '{0}' cannot be suppressed. + var message = string.Format(CodeAnalysisResources.NonReportedDiagnosticCannotBeSuppressed, suppression.SuppressedDiagnostic.Id); + throw new ArgumentException(message); + } + + if (!_isSupportedSuppressionDescriptor(suppression.Descriptor)) + { + // Reported suppression with ID '{0}' is not supported by the suppressor. + var message = string.Format(CodeAnalysisResources.UnsupportedSuppressionReported, suppression.Descriptor.Id); + throw new ArgumentException(message); + } + + if (suppression.Descriptor.IsDisabled(Compilation.Options)) + { + // Suppression has been disabled by the end user through compilation options. + return; + } + + _addSuppression(suppression); + } + + /// + /// Gets a for the given , which is shared across all analyzers. + /// + public SemanticModel GetSemanticModel(SyntaxTree syntaxTree) => _getSemanticModel(syntaxTree); + } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticSuppressor.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticSuppressor.cs new file mode 100644 index 0000000000000..c270270855f22 --- /dev/null +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticSuppressor.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.Diagnostics +{ + /// + /// The base type for diagnostic suppressors that can programmatically suppress analyzer and/or compiler non-error diagnostics. + /// + public abstract class DiagnosticSuppressor : DiagnosticAnalyzer + { + // Disallow suppressors from reporting diagnostics or registering analysis actions. + public sealed override ImmutableArray SupportedDiagnostics => ImmutableArray.Empty; + + public sealed override void Initialize(AnalysisContext context) { } + + /// + /// Returns a set of descriptors for the suppressions that this suppressor is capable of producing. + /// + public abstract ImmutableArray SupportedSuppressions { get; } + + /// + /// Suppress analyzer and/or compiler non-error diagnostics reported for the compilation. + /// This may be a subset of the full set of reported diagnostics, as an optimization for + /// supporting incremental and partial analysis scenarios. + /// A diagnostic is considered suppressible by a DiagnosticSuppressor if *all* of the following conditions are met: + /// 1. Diagnostic is not already suppressed in source via pragma/suppress message attribute. + /// 2. Diagnostic's is not . + /// 3. Diagnostic is not tagged with custom tag. + /// + public abstract void ReportSuppressions(SuppressionAnalysisContext context); + } +} diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/Suppression.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/Suppression.cs new file mode 100644 index 0000000000000..9095adb134cdc --- /dev/null +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/Suppression.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; + +namespace Microsoft.CodeAnalysis.Diagnostics +{ + /// + /// Programmatic suppression of a by a . + /// + public struct Suppression + { + private Suppression(SuppressionDescriptor descriptor, Diagnostic suppressedDiagnostic) + { + Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + SuppressedDiagnostic = suppressedDiagnostic ?? throw new ArgumentNullException(nameof(suppressedDiagnostic)); + Debug.Assert(suppressedDiagnostic.ProgrammaticSuppressionInfo == null); + + if (descriptor.SuppressedDiagnosticId != suppressedDiagnostic.Id) + { + // Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + var message = string.Format(CodeAnalysisResources.InvalidDiagnosticSuppressionReported, suppressedDiagnostic.Id, descriptor.SuppressedDiagnosticId); + throw new ArgumentException(message); + } + } + + /// + /// Creates a suppression of a with the given . + /// + /// + /// Descriptor for the suppression, which must be from + /// for the creating this suppression. + /// + /// + /// to be suppressed, which must be from + /// for the suppression context in which this suppression is being created. + public static Suppression Create(SuppressionDescriptor descriptor, Diagnostic suppressedDiagnostic) + => new Suppression(descriptor, suppressedDiagnostic); + + /// + /// Descriptor for this suppression. + /// + public SuppressionDescriptor Descriptor { get; } + + /// + /// Diagnostic suppressed by this suppression. + /// + public Diagnostic SuppressedDiagnostic { get; } + } +} diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 118f2cd5c6c18..2a2aaf1ae44f5 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -3,6 +3,20 @@ Microsoft.CodeAnalysis.CommandLineArguments.EmitPdbFile.get -> bool Microsoft.CodeAnalysis.CommandLineArguments.GetOutputFilePath(string outputFileName) -> string Microsoft.CodeAnalysis.CommandLineArguments.GetPdbFilePath(string outputFileName) -> string +Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor +Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor.DiagnosticSuppressor() -> void +Microsoft.CodeAnalysis.Diagnostics.Suppression +Microsoft.CodeAnalysis.Diagnostics.Suppression.Descriptor.get -> Microsoft.CodeAnalysis.SuppressionDescriptor +Microsoft.CodeAnalysis.Diagnostics.Suppression.SuppressedDiagnostic.get -> Microsoft.CodeAnalysis.Diagnostic +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext.CancellationToken.get -> System.Threading.CancellationToken +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext.Compilation.get -> Microsoft.CodeAnalysis.Compilation +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext.GetSemanticModel(Microsoft.CodeAnalysis.SyntaxTree syntaxTree) -> Microsoft.CodeAnalysis.SemanticModel +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext.Options.get -> Microsoft.CodeAnalysis.Diagnostics.AnalyzerOptions +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext.ReportSuppression(Microsoft.CodeAnalysis.Diagnostics.Suppression suppression) -> void +Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext.ReportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.Diagnostics.Telemetry.AnalyzerTelemetryInfo.SuppressionActionsCount.get -> int +Microsoft.CodeAnalysis.Diagnostics.Telemetry.AnalyzerTelemetryInfo.SuppressionActionsCount.set -> void Microsoft.CodeAnalysis.IArrayTypeSymbol.ElementNullableAnnotation.get -> Microsoft.CodeAnalysis.NullableAnnotation Microsoft.CodeAnalysis.IDiscardSymbol.NullableAnnotation.get -> Microsoft.CodeAnalysis.NullableAnnotation Microsoft.CodeAnalysis.IEventSymbol.NullableAnnotation.get -> Microsoft.CodeAnalysis.NullableAnnotation @@ -33,6 +47,13 @@ Microsoft.CodeAnalysis.NullableFlowState Microsoft.CodeAnalysis.NullableFlowState.MaybeNull = 2 -> Microsoft.CodeAnalysis.NullableFlowState Microsoft.CodeAnalysis.NullableFlowState.NotApplicable = 0 -> Microsoft.CodeAnalysis.NullableFlowState Microsoft.CodeAnalysis.NullableFlowState.NotNull = 1 -> Microsoft.CodeAnalysis.NullableFlowState +Microsoft.CodeAnalysis.SuppressionDescriptor +Microsoft.CodeAnalysis.SuppressionDescriptor.Equals(Microsoft.CodeAnalysis.SuppressionDescriptor other) -> bool +Microsoft.CodeAnalysis.SuppressionDescriptor.Id.get -> string +Microsoft.CodeAnalysis.SuppressionDescriptor.Justification.get -> Microsoft.CodeAnalysis.LocalizableString +Microsoft.CodeAnalysis.SuppressionDescriptor.SuppressedDiagnosticId.get -> string +Microsoft.CodeAnalysis.SuppressionDescriptor.SuppressionDescriptor(string id, string suppressedDiagnosticId, Microsoft.CodeAnalysis.LocalizableString justification) -> void +Microsoft.CodeAnalysis.SuppressionDescriptor.SuppressionDescriptor(string id, string suppressedDiagnosticId, string justification) -> void Microsoft.CodeAnalysis.TypeInfo.ConvertedNullability.get -> Microsoft.CodeAnalysis.NullabilityInfo Microsoft.CodeAnalysis.TypeInfo.Nullability.get -> Microsoft.CodeAnalysis.NullabilityInfo abstract Microsoft.CodeAnalysis.Compilation.ClassifyCommonConversion(Microsoft.CodeAnalysis.ITypeSymbol source, Microsoft.CodeAnalysis.ITypeSymbol destination) -> Microsoft.CodeAnalysis.Operations.CommonConversion @@ -41,6 +62,8 @@ abstract Microsoft.CodeAnalysis.Compilation.GetSymbolsWithName(string name, Micr abstract Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions.TryGetValue(string key, out string value) -> bool abstract Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider.GetOptions(Microsoft.CodeAnalysis.AdditionalText textFile) -> Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions abstract Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptionsProvider.GetOptions(Microsoft.CodeAnalysis.SyntaxTree tree) -> Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions +abstract Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor.ReportSuppressions(Microsoft.CodeAnalysis.Diagnostics.SuppressionAnalysisContext context) -> void +abstract Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor.SupportedSuppressions.get -> System.Collections.Immutable.ImmutableArray abstract Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext.RegisterCodeBlockAction(System.Action action) -> void abstract Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext.RegisterCodeBlockStartAction(System.Action> action) -> void abstract Microsoft.CodeAnalysis.Diagnostics.SymbolStartAnalysisContext.RegisterOperationAction(System.Action action, System.Collections.Immutable.ImmutableArray operationKinds) -> void @@ -259,6 +282,10 @@ Microsoft.CodeAnalysis.SymbolDisplayPartKind.ExtensionMethodName = 29 -> Microso const Microsoft.CodeAnalysis.WellKnownMemberNames.SliceMethodName = "Slice" -> string override Microsoft.CodeAnalysis.FlowAnalysis.CaptureId.Equals(object obj) -> bool override Microsoft.CodeAnalysis.FlowAnalysis.CaptureId.GetHashCode() -> int +override Microsoft.CodeAnalysis.SuppressionDescriptor.Equals(object obj) -> bool +override Microsoft.CodeAnalysis.SuppressionDescriptor.GetHashCode() -> int +override sealed Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext context) -> void +override sealed Microsoft.CodeAnalysis.Diagnostics.DiagnosticSuppressor.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray static Microsoft.CodeAnalysis.AnalyzerConfig.Parse(Microsoft.CodeAnalysis.Text.SourceText text, string pathToFile) -> Microsoft.CodeAnalysis.AnalyzerConfig static Microsoft.CodeAnalysis.AnalyzerConfig.Parse(string text, string pathToFile) -> Microsoft.CodeAnalysis.AnalyzerConfig static Microsoft.CodeAnalysis.AnalyzerConfig.ReservedKeys.get -> System.Collections.Immutable.ImmutableHashSet @@ -270,6 +297,7 @@ static Microsoft.CodeAnalysis.AnalyzerConfigSet.Create(TList analyzerConf static Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions.KeyComparer.get -> System.StringComparer override Microsoft.CodeAnalysis.NullabilityInfo.Equals(object other) -> bool override Microsoft.CodeAnalysis.NullabilityInfo.GetHashCode() -> int +static Microsoft.CodeAnalysis.Diagnostics.Suppression.Create(Microsoft.CodeAnalysis.SuppressionDescriptor descriptor, Microsoft.CodeAnalysis.Diagnostic suppressedDiagnostic) -> Microsoft.CodeAnalysis.Diagnostics.Suppression static Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowGraph.Create(Microsoft.CodeAnalysis.Operations.IBlockOperation body, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowGraph static Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowGraph.Create(Microsoft.CodeAnalysis.Operations.IConstructorBodyOperation constructorBody, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowGraph static Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowGraph.Create(Microsoft.CodeAnalysis.Operations.IFieldInitializerOperation initializer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.FlowAnalysis.ControlFlowGraph diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf index a7a8b9e71c3f9..c937f6db41419 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf @@ -19,6 +19,11 @@ {1}. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Blok dané operace nepatří do aktuálního analytického kontextu. @@ -34,6 +39,11 @@ Parametr {0} musí být symbol z této kompilace nebo některé odkazované sestavení. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. Daná operace má nadřazenou položku, která není null. @@ -74,6 +84,26 @@ Analyzátor {0} má v SupportedDiagnostics deskriptor s hodnotou null. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Nevyřešeno: @@ -204,6 +234,11 @@ {0}.GetMetadata() musí vracet instanci {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Hodnota je moc velká, než aby se dala vyjádřit jako 30bitové nepodepsané celé číslo. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf index 9e4a8569c6123..e1ab670115998 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf @@ -19,6 +19,11 @@ "{1}". + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Der angegebene Operationsblock gehört nicht zum aktuellen Analysekontext. @@ -34,6 +39,11 @@ Der Parameter "{0}" muss ein Symbol aus dieser Zusammenstellung oder eine referenzierte Assembly sein. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. Die angegebene Operation weist ein übergeordnetes Element ungleich NULL auf. @@ -74,6 +84,26 @@ Der Analyzer "{0}" enthält einen NULL-Deskriptor in "SupportedDiagnostics". + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Nicht aufgelöst: @@ -204,6 +234,11 @@ {0}.GetMetadata() muss eine Instanz von {1} zurückgeben. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Der Wert ist zu groß, um als ganze 30-Bit-Zahl ohne Vorzeichen dargestellt zu werden. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf index 377d3f97e9fe1..d7bb6a59216ad 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf @@ -19,6 +19,11 @@ '{1}'. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. El bloque de operaciones dado no pertenece al contexto de análisis actual. @@ -34,6 +39,11 @@ El parámetro "{0}" debe ser un símbolo de esta compilación o algún ensamble al que se hace referencia. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. La operación dada tiene un elemento primario no nulo. @@ -74,6 +84,26 @@ El analizador de "{0}" contiene un descriptor nulo en "SupportedDiagnostics". + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Sin resolver: @@ -204,6 +234,11 @@ {0}.GetMetadata() debe devolver una instancia de {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Valor demasiado largo para representarse como entero sin signo de 30 bits. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf index 2c2d1d5cb3b23..4a7bf430f9209 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf @@ -19,6 +19,11 @@ '{1}'. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Le bloc d'opérations donné n'appartient pas au contexte d'analyse actuel. @@ -34,6 +39,11 @@ Le paramètre '{0}' doit être un symbole de cette compilation ou un assembly référencé. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. L'opération donnée a un parent non-null. @@ -74,6 +84,26 @@ L'analyseur '{0}' contient un descripteur null dans 'SupportedDiagnostics'. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Non résolu : @@ -204,6 +234,11 @@ {0}.GetMetadata() doit retourner une instance de {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. La valeur est trop grande pour être représentée comme un entier non signé 30 bits. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf index a31bdc906106e..861bf631ce8ff 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf @@ -19,6 +19,11 @@ '{1}'. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Il blocco operazioni specificato non appartiene al contesto di analisi corrente. @@ -34,6 +39,11 @@ Il parametro '{0}' deve essere un simbolo di questa compilazione oppure un qualsiasi assembly cui viene fatto riferimento. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. L'operazione specificata contiene un elemento padre non Null. @@ -74,6 +84,26 @@ L'analizzatore '{0}' contiene un descrittore Null nel relativo elemento 'SupportedDiagnostics'. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Non risolto: @@ -204,6 +234,11 @@ {0}.GetMetadata() deve restituire un'istanza di {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Il valore è troppo grande per essere rappresentato come intero senza segno a 30 bit. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf index 2204d1fbcc54d..935cc4f39d161 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf @@ -19,6 +19,11 @@ '{1}'。 + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. 指定した操作ブロックが現在の分析コンテストに属していません。 @@ -34,6 +39,11 @@ パラメーター '{0}' は、このコンパイルまたはいくつかの参照アセンブリのシンボルにする必要があります。 + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. 指定した操作の親が null 以外です。 @@ -74,6 +84,26 @@ アナライザー '{0}' の 'SupportedDiagnostics' に null 記述子が含まれています。 + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: 未解決: @@ -204,6 +234,11 @@ {0}.GetMetadata() は {1} のインスタンスを返す必要があります。 + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. 値が大きすぎるため、30 ビットの符号なし整数として表すことができません。 diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf index 90561dc0afd1f..ae0c9b74cda0d 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf @@ -19,6 +19,11 @@ '{1}' + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. 지정한 작업 블록이 현재 분석 컨텍스트에 속하지 않습니다. @@ -34,6 +39,11 @@ '{0}' 매개 변수는 이 컴파일 또는 일부 참조된 어셈블리의 기호여야 합니다. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. 지정한 작업에 null이 아닌 부모가 있습니다. @@ -74,6 +84,26 @@ '{0}' 분석기의 'SupportedDiagnostics'에 null 설명자가 포함되어 있습니다. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: 확인되지 않음: @@ -204,6 +234,11 @@ {0}.GetMetadata()는 {1}의 인스턴스를 반환해야 합니다. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. 값이 너무 커서 30비트 정수로 표시할 수 없습니다. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf index a581ff10ca146..c815dcb8c5fe6 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf @@ -19,6 +19,11 @@ „{1}”. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Dany blok operacji nie należy do bieżącego kontekstu analizy. @@ -34,6 +39,11 @@ Parametr „{0}” musi być symbolem z tej kompilacji lub przywoływanym zestawem. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. Dana operacja ma element nadrzędny inny niż null. @@ -74,6 +84,26 @@ Analizator „{0}” zawiera deskryptor null we właściwości „SupportedDiagnostics”. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Nierozpoznane: @@ -204,6 +234,11 @@ Metoda {0}.GetMetadata() musi zwrócić wystąpienie {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Wartość jest zbyt duża, dlatego nie może być reprezentowana jako 30-bitowa liczba całkowita bez znaku. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf index 138ee21dbc0c1..9a9f74dbdb233 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf @@ -19,6 +19,11 @@ '{1}'. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. O bloqueio de operação fornecido não pertence ao contexto de análise atual. @@ -34,6 +39,11 @@ O parâmetro '{0}' deve ser um símbolo desta compilação ou um assembly referenciado. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. A operação especificada tem um pai não nulo. @@ -74,6 +84,26 @@ O analisador '{0}' contém um descritor nulo em seu 'SupportedDiagnostics'. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Não resolvido: @@ -204,6 +234,11 @@ {0}.GetMetadata() deve retornar uma instância de {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Valor muito grande para ser representado como um inteiro não assinado de 30 bits. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf index af85f0e28c010..31e934ef5742e 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf @@ -19,6 +19,11 @@ "{1}". + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Заданный блок операции не принадлежит текущему контексту анализа. @@ -34,6 +39,11 @@ Параметр "{0}" должен быть символом из этой компиляции или из другой сборки, на которую она ссылается. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. Заданная операция имеет родительский элемент, отличный от NULL. @@ -74,6 +84,26 @@ Анализатор "{0}" содержит дескриптор null в разделе "SupportedDiagnostics". + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Не разрешено: @@ -204,6 +234,11 @@ {0}.GetMetadata() должен возвращать экземпляр {1}. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Слишком большое значение для представления в виде 30-разрядного целого числа без знака. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf index d02c3e4f58c9c..81a1779490d1f 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf @@ -19,6 +19,11 @@ '{1}'. + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. Belirtilen işlem bloğu geçerli analiz bağlamına ait değil. @@ -34,6 +39,11 @@ '{0}' parametresi bu derleme veya bazı başvurulan derleme bir sembol olması gerekir. + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. Belirtilen işlemin null olmayan bir üst öğesi var. @@ -74,6 +84,26 @@ Çözümleyicisi '{0}', 'SupportedDiagnostics' boş bir tanımlayıcısı içerir. + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: Çözümlenmemiş: @@ -204,6 +234,11 @@ {0}.GetMetadata() bir {1} örneği döndürmelidir. + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. Değer, 30 bit işaretsiz tamsayı olarak temsil edilemeyecek kadar büyük. diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf index 251026bcc5260..1b5d1e8cab659 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf @@ -19,6 +19,11 @@ “{1}”。 + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. 给定操作块不属于当前的分析上下文。 @@ -34,6 +39,11 @@ 参数“{0}”必须是此编译或某些引用程序集的符号。 + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. 给定操作具有一个非 null 父级。 @@ -74,6 +84,26 @@ 分析器“{0}”在其 "SupportedDiagnostics" 中包含 null 描述符。 + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: 未解析: @@ -204,6 +234,11 @@ {0}.GetMetadata() 必须返回 {1} 的实例。 + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. 值太大,无法表示为 30 位无符号整数。 diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf index 98bf04e3c083e..04f98da3f5a70 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf @@ -19,6 +19,11 @@ '{1}'。 + + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + Suppressed diagnostic ID '{0}' does not match suppressable ID '{1}' for the given suppression descriptor. + + Given operation block does not belong to the current analysis context. 指定的作業區塊不屬於目前的分析內容。 @@ -34,6 +39,11 @@ 參數 '{0}' 必須是此編譯或某些參考組件中的符號。 + + Non-reported diagnostic with ID '{0}' cannot be suppressed. + Non-reported diagnostic with ID '{0}' cannot be suppressed. + + Given operation has a non-null parent. 指定的作業有非 null 的父代。 @@ -74,6 +84,26 @@ 分析器 '{0}' 在其 'SupportedDiagnostics' 中包含一個 null 描述項。 + + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + Analyzer '{0}' contains a null descriptor in its 'SupportedSuppressions'. + + + + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + + + + Programmatic suppression of an analyzer diagnostic + Programmatic suppression of an analyzer diagnostic + + + + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + A SuppressionDescriptor must have an Id that is neither null nor an empty string nor a string that only contains white space. + + Unresolved: 未解析: @@ -204,6 +234,11 @@ {0}.GetMetadata() 必須傳回 {1} 的執行個體。 + + Reported suppression with ID '{0}' is not supported by the suppressor. + Reported suppression with ID '{0}' is not supported by the suppressor. + + Value too large to be represented as a 30 bit unsigned integer. 值太大,無法呈現為 30 位元不帶正負號的整數。 diff --git a/src/Compilers/Test/Utilities/CSharp/TestOptions.cs b/src/Compilers/Test/Utilities/CSharp/TestOptions.cs index b4a400528439a..1b881553e0753 100644 --- a/src/Compilers/Test/Utilities/CSharp/TestOptions.cs +++ b/src/Compilers/Test/Utilities/CSharp/TestOptions.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Linq; using Roslyn.Test.Utilities; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; namespace Microsoft.CodeAnalysis.CSharp.Test.Utilities diff --git a/src/Compilers/Test/Utilities/VisualBasic/TestOptions.vb b/src/Compilers/Test/Utilities/VisualBasic/TestOptions.vb index 56d1327841e6b..c0f53e7162ed7 100644 --- a/src/Compilers/Test/Utilities/VisualBasic/TestOptions.vb +++ b/src/Compilers/Test/Utilities/VisualBasic/TestOptions.vb @@ -1,6 +1,7 @@ ' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. Imports System.Runtime.CompilerServices +Imports Microsoft.CodeAnalysis.Diagnostics Imports Microsoft.CodeAnalysis.VisualBasic.Syntax.InternalSyntax Imports Roslyn.Test.Utilities diff --git a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb index 64c3525c282a5..a1f9064511d2a 100644 --- a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb +++ b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb @@ -7601,7 +7601,8 @@ BC2006: option 'analyzerconfig' requires ':']]> Optional additionalFlags As String() = Nothing, Optional expectedInfoCount As Integer = 0, Optional expectedWarningCount As Integer = 0, - Optional expectedErrorCount As Integer = 0) As String + Optional expectedErrorCount As Integer = 0, + Optional analyzers As ImmutableArray(Of DiagnosticAnalyzer) = Nothing) As String Dim args = { "/nologo", "/preferreduilang:en", "/t:library", sourceFile.Path @@ -7613,7 +7614,7 @@ BC2006: option 'analyzerconfig' requires ':']]> args = args.Append(additionalFlags) End If - Dim vbc = New MockVisualBasicCompiler(Nothing, sourceDir.Path, args) + Dim vbc = New MockVisualBasicCompiler(Nothing, sourceDir.Path, args, analyzers) Dim outWriter = New StringWriter(CultureInfo.InvariantCulture) Dim exitCode = vbc.Run(outWriter, Nothing) Dim output = outWriter.ToString() @@ -9338,6 +9339,224 @@ End Class").Path Assert.Equal(1, exitCode) Assert.Contains("vbc : error BC37253: The pathmap option was incorrectly formatted.", outWriter.ToString(), StringComparison.Ordinal) End Sub + + + + Public Sub TestSuppression_CompilerWarning() + ' 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("a.vb") + file.WriteAllText(source) + + ' Verify that compiler warning BC40008 is reported. + Dim output = VerifyOutput(dir, file, expectedWarningCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False) + Assert.Contains("warning BC40008", output, StringComparison.Ordinal) + + ' Verify that compiler warning BC40008 is suppressed with diagnostic suppressor + ' and info diagnostic is logged with programmatic suppression information. + Dim suppressor = New DiagnosticSuppressorForId("BC40008") + + ' Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Dim suppressionMessage = String.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + New VBDiagnostic(ErrorFactory.ErrorInfo(ERRID.WRN_UseOfObsoleteSymbolNoMessage1, "C"), Location.None).GetMessage(CultureInfo.InvariantCulture), + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification) + + Dim suppressors = ImmutableArray.Create(Of DiagnosticAnalyzer)(suppressor) + output = VerifyOutput(dir, file, expectedInfoCount:=1, expectedWarningCount:=0, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=suppressors) + Assert.DoesNotContain("warning BC40008", output, StringComparison.Ordinal) + Assert.Contains("info SP0001", output, StringComparison.Ordinal) + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal) + + CleanupAllGeneratedFiles(file.Path) + End Sub + + + + Public Sub TestSuppression_CompilerWarningAsError() + ' 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("a.vb") + file.WriteAllText(source) + + ' Verify that compiler warning BC40008 is reported. + Dim output = VerifyOutput(dir, file, expectedWarningCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False) + Assert.Contains("warning BC40008", output, StringComparison.Ordinal) + + ' Verify that compiler warning BC40008 is reported as error for /warnaserror. + output = VerifyOutput(dir, file, expectedErrorCount:=1, additionalFlags:={"/warnaserror+"}, + includeCurrentAssemblyAsAnalyzerReference:=False) + Assert.Contains("error BC40008", output, StringComparison.Ordinal) + + ' Verify that compiler warning BC40008 is suppressed with diagnostic suppressor even with /warnaserror + ' and info diagnostic is logged with programmatic suppression information. + Dim suppressor = New DiagnosticSuppressorForId("BC40008") + + Dim suppressors = ImmutableArray.Create(Of DiagnosticAnalyzer)(suppressor) + output = VerifyOutput(dir, file, expectedInfoCount:=1, expectedWarningCount:=0, expectedErrorCount:=0, + additionalFlags:={"/warnaserror+"}, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=suppressors) + Assert.DoesNotContain($"warning BC40008", output, StringComparison.Ordinal) + Assert.DoesNotContain($"error BC40008", output, StringComparison.Ordinal) + + ' Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Dim suppressionMessage = String.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + New VBDiagnostic(ErrorFactory.ErrorInfo(ERRID.WRN_UseOfObsoleteSymbolNoMessage1, "C"), Location.None).GetMessage(CultureInfo.InvariantCulture), + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification) + Assert.Contains("info SP0001", output, StringComparison.Ordinal) + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal) + + CleanupAllGeneratedFiles(file.Path) + End Sub + + + + Public Sub TestNoSuppression_CompilerError() + ' warning BC30203 : Identifier expected + Dim source = " +Class +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("a.vb") + file.WriteAllText(source) + + ' Verify that compiler error BC30203 is reported. + Dim output = VerifyOutput(dir, file, expectedErrorCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False) + Assert.Contains("error BC30203", output, StringComparison.Ordinal) + + ' Verify that compiler error BC30203 cannot be suppressed with diagnostic suppressor. + Dim analyzers = ImmutableArray.Create(Of DiagnosticAnalyzer)(New DiagnosticSuppressorForId("BC30203")) + output = VerifyOutput(dir, file, expectedErrorCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzers) + Assert.Contains("error BC30203", output, StringComparison.Ordinal) + + CleanupAllGeneratedFiles(file.Path) + End Sub + + + + Public Sub TestSuppression_AnalyzerWarning() + Dim source = " +Class C +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("a.vb") + file.WriteAllText(source) + + ' Verify that analyzer warning is reported. + Dim analyzer = New CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable:=True) + Dim analyzers = ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer) + Dim output = VerifyOutput(dir, file, expectedWarningCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzers) + Assert.Contains($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + + ' Verify that analyzer warning is suppressed with diagnostic suppressor + ' and info diagnostic is logged with programmatic suppression information. + Dim suppressor = New DiagnosticSuppressorForId(analyzer.Descriptor.Id) + + ' Diagnostic '{0}: {1}' was programmatically suppressed by a DiagnosticSuppressor with suppresion ID '{2}' and justification '{3}' + Dim suppressionMessage = String.Format(CodeAnalysisResources.SuppressionDiagnosticDescriptorMessage, + suppressor.SuppressionDescriptor.SuppressedDiagnosticId, + analyzer.Descriptor.MessageFormat, + suppressor.SuppressionDescriptor.Id, + suppressor.SuppressionDescriptor.Justification) + + Dim analyzerAndSuppressor = ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer, suppressor) + output = VerifyOutput(dir, file, expectedInfoCount:=1, expectedWarningCount:=0, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzerAndSuppressor) + Assert.DoesNotContain($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + Assert.Contains("info SP0001", output, StringComparison.Ordinal) + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal) + + ' Verify that analyzer warning is reported as error for /warnaserror. + output = VerifyOutput(dir, file, expectedErrorCount:=1, + additionalFlags:={"/warnaserror+"}, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzers) + Assert.Contains($"error {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + + ' Verify that analyzer warning is suppressed with diagnostic suppressor even with /warnaserror + ' and info diagnostic is logged with programmatic suppression information. + output = VerifyOutput(dir, file, expectedInfoCount:=1, expectedWarningCount:=0, expectedErrorCount:=0, + additionalFlags:={"/warnaserror+"}, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzerAndSuppressor) + Assert.DoesNotContain($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + Assert.Contains("info SP0001", output, StringComparison.Ordinal) + Assert.Contains(suppressionMessage, output, StringComparison.Ordinal) + + ' Verify that "NotConfigurable" analyzer warning cannot be suppressed with diagnostic suppressor even with /warnaserror. + analyzer = New CompilationAnalyzerWithSeverity(DiagnosticSeverity.Warning, configurable:=False) + suppressor = New DiagnosticSuppressorForId(analyzer.Descriptor.Id) + analyzerAndSuppressor = ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer, suppressor) + output = VerifyOutput(dir, file, expectedWarningCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzerAndSuppressor) + Assert.Contains($"warning {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + + CleanupAllGeneratedFiles(file.Path) + End Sub + + + + Public Sub TestNoSuppression_AnalyzerError() + Dim source = " +Class C +End Class" + Dim dir = Temp.CreateDirectory() + Dim file = dir.CreateFile("a.vb") + file.WriteAllText(source) + + ' Verify that analyzer error is reported. + Dim analyzer = New CompilationAnalyzerWithSeverity(DiagnosticSeverity.Error, configurable:=True) + Dim analyzers = ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer) + Dim output = VerifyOutput(dir, file, expectedErrorCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzers) + Assert.Contains($"error {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + + ' Verify that analyzer error cannot be suppressed with diagnostic suppressor. + Dim suppressor = New DiagnosticSuppressorForId(analyzer.Descriptor.Id) + Dim analyzerAndSuppressor = ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer, suppressor) + output = VerifyOutput(dir, file, expectedErrorCount:=1, + includeCurrentAssemblyAsAnalyzerReference:=False, + analyzers:=analyzerAndSuppressor) + Assert.Contains($"error {analyzer.Descriptor.Id}", output, StringComparison.Ordinal) + + CleanupAllGeneratedFiles(file.Path) + End Sub End Class diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb index 6e6f01c36353f..7a34a2cbd9b78 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb @@ -1989,7 +1989,7 @@ class MyClass Dim project = workspace.CurrentSolution.Projects.Single() ' Add analyzer - Dim analyzer = New HiddenDiagnosticsCompilationAnalyzer() + Dim analyzer = New CompilationAnalyzerWithSeverity(DiagnosticSeverity.Hidden, configurable:=False) Dim analyzerReference = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer)) project = project.AddAnalyzerReference(analyzerReference) @@ -2002,7 +2002,7 @@ class MyClass Assert.Equal(1, descriptorsMap.Count) Dim descriptors = descriptorsMap.First().Value Assert.Equal(1, descriptors.Length) - Assert.Equal(HiddenDiagnosticsCompilationAnalyzer.Descriptor.Id, descriptors.Single().Id) + Assert.Equal(analyzer.Descriptor.Id, descriptors.Single().Id) ' Force project analysis incrementalAnalyzer.AnalyzeProjectAsync(project, semanticsChanged:=True, reasons:=InvocationReasons.Empty, cancellationToken:=CancellationToken.None).Wait() @@ -2018,7 +2018,7 @@ class MyClass ' Get diagnostics explicitly Dim hiddenDiagnostics = diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id).WaitAndGetResult(CancellationToken.None) Assert.Equal(1, hiddenDiagnostics.Count()) - Assert.Equal(HiddenDiagnosticsCompilationAnalyzer.Descriptor.Id, hiddenDiagnostics.Single().Id) + Assert.Equal(analyzer.Descriptor.Id, hiddenDiagnostics.Single().Id) End Using End Sub diff --git a/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs b/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs index 024068066afce..a4b3e6c0f733d 100644 --- a/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs +++ b/src/Features/Core/Portable/Diagnostics/AnalyzerHelper.cs @@ -357,6 +357,9 @@ public static async Task CreateAnalyzerDriverAsync( Contract.ThrowIfFalse(project.SupportsCompilation); AssertCompilation(project, compilation); + // Always run diagnostic suppressors. + analyzers = AppendDiagnosticSuppressors(analyzers, allAnalyzersAndSuppressors: service.GetDiagnosticAnalyzers(project)); + // Create driver that holds onto compilation and associated analyzers return compilation.WithAnalyzers(filteredAnalyzers, GetAnalyzerOptions()); @@ -788,6 +791,15 @@ IEnumerable ConvertToLocalDiagnosticsWithCompilation() } } + public static IEnumerable AppendDiagnosticSuppressors(IEnumerable analyzers, IEnumerable allAnalyzersAndSuppressors) + { + // Append while ensuring no duplicates are added. + var diagnosticSuppressors = allAnalyzersAndSuppressors.OfType(); + return !diagnosticSuppressors.Any() + ? analyzers + : analyzers.Concat(diagnosticSuppressors).Distinct(); + } + /// /// Right now, there is no API compiler will tell us whether DiagnosticAnalyzer has compilation end analysis or not /// diff --git a/src/Features/Core/Portable/Diagnostics/DiagnosticResultSerializer.cs b/src/Features/Core/Portable/Diagnostics/DiagnosticResultSerializer.cs index c05f6de8d43ff..9ba1d3697e6a1 100644 --- a/src/Features/Core/Portable/Diagnostics/DiagnosticResultSerializer.cs +++ b/src/Features/Core/Portable/Diagnostics/DiagnosticResultSerializer.cs @@ -180,6 +180,7 @@ private static void Serialize(ObjectWriter writer, AnalyzerTelemetryInfo telemet writer.WriteInt32(telemetryInfo.OperationBlockActionsCount); writer.WriteInt32(telemetryInfo.OperationBlockStartActionsCount); writer.WriteInt32(telemetryInfo.OperationBlockEndActionsCount); + writer.WriteInt32(telemetryInfo.SuppressionActionsCount); writer.WriteInt64(telemetryInfo.ExecutionTime.Ticks); writer.WriteBoolean(telemetryInfo.Concurrent); } @@ -204,6 +205,7 @@ private static AnalyzerTelemetryInfo Deserialize(ObjectReader reader, Cancellati var operationBlockActionsCount = reader.ReadInt32(); var operationBlockStartActionsCount = reader.ReadInt32(); var operationBlockEndActionsCount = reader.ReadInt32(); + var suppressionActionsCount = reader.ReadInt32(); var executionTime = new TimeSpan(reader.ReadInt64()); var concurrent = reader.ReadBoolean(); @@ -229,6 +231,8 @@ private static AnalyzerTelemetryInfo Deserialize(ObjectReader reader, Cancellati OperationBlockEndActionsCount = operationBlockEndActionsCount, OperationBlockActionsCount = operationBlockActionsCount, + SuppressionActionsCount = suppressionActionsCount, + ExecutionTime = executionTime, Concurrent = concurrent diff --git a/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogAggregator.cs b/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogAggregator.cs index ac877757c73dd..792daf942deb6 100644 --- a/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogAggregator.cs +++ b/src/Features/Core/Portable/Diagnostics/Log/DiagnosticLogAggregator.cs @@ -28,6 +28,7 @@ internal class DiagnosticLogAggregator : LogAggregator "Analyzer.OperationBlockStart", "Analyzer.SymbolEnd", "Analyzer.SymbolStart", + "Analyzer.Suppression", }; private readonly DiagnosticAnalyzerService _owner; @@ -87,6 +88,7 @@ public void SetAnalyzerTypeCount(AnalyzerTelemetryInfo analyzerTelemetryInfo) Counts[13] = analyzerTelemetryInfo.OperationBlockStartActionsCount; Counts[14] = analyzerTelemetryInfo.SymbolStartActionsCount; Counts[15] = analyzerTelemetryInfo.SymbolEndActionsCount; + Counts[16] = analyzerTelemetryInfo.SuppressionActionsCount; } } } diff --git a/src/Test/Utilities/Portable/Diagnostics/CommonDiagnosticAnalyzers.cs b/src/Test/Utilities/Portable/Diagnostics/CommonDiagnosticAnalyzers.cs index a27e2a8f8591f..b799ac14f1cc9 100644 --- a/src/Test/Utilities/Portable/Diagnostics/CommonDiagnosticAnalyzers.cs +++ b/src/Test/Utilities/Portable/Diagnostics/CommonDiagnosticAnalyzers.cs @@ -703,16 +703,24 @@ public override void Initialize(AnalysisContext context) } [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] - public class HiddenDiagnosticsCompilationAnalyzer : DiagnosticAnalyzer + public class CompilationAnalyzerWithSeverity : DiagnosticAnalyzer { - public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - "ID1000", - "Description1", - string.Empty, - "Analysis", - DiagnosticSeverity.Hidden, - true, - customTags: WellKnownDiagnosticTags.NotConfigurable); + public CompilationAnalyzerWithSeverity( + DiagnosticSeverity severity, + bool configurable) + { + var customTags = !configurable ? new[] { WellKnownDiagnosticTags.NotConfigurable } : Array.Empty(); + Descriptor = new DiagnosticDescriptor( + "ID1000", + "Description1", + string.Empty, + "Analysis", + severity, + true, + customTags: customTags); + } + + public DiagnosticDescriptor Descriptor { get; } public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); @@ -723,7 +731,7 @@ public override void Initialize(AnalysisContext context) private void OnCompilation(CompilationAnalysisContext context) { - // Report the hidden diagnostic on all trees in compilation. + // Report the diagnostic on all trees in compilation. foreach (var tree in context.Compilation.SyntaxTrees) { context.ReportDiagnostic(Diagnostic.Create(Descriptor, tree.GetRoot().GetLocation())); @@ -1661,5 +1669,162 @@ void verifySymbolStartAndOperationOrdering(SymbolStartAnalysisContext symbolStar } } } + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DiagnosticSuppressorForId : DiagnosticSuppressor + { + public SuppressionDescriptor SuppressionDescriptor { get; } + public DiagnosticSuppressorForId(string suppressedDiagnosticId, string suppressionId = null) + { + SuppressionDescriptor = new SuppressionDescriptor( + id: suppressionId ?? "SPR0001", + suppressedDiagnosticId: suppressedDiagnosticId, + justification: $"Suppress {suppressedDiagnosticId}"); + } + + public override ImmutableArray SupportedSuppressions + => ImmutableArray.Create(SuppressionDescriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + Assert.Equal(SuppressionDescriptor.SuppressedDiagnosticId, diagnostic.Id); + context.ReportSuppression(Suppression.Create(SuppressionDescriptor, diagnostic)); + } + } + } + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DiagnosticSuppressorThrowsExceptionFromSupportedSuppressions : DiagnosticSuppressor + { + public override ImmutableArray SupportedSuppressions + => throw new NotImplementedException(); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + } + } + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DiagnosticSuppressorThrowsExceptionFromReportedSuppressions : DiagnosticSuppressor + { + private readonly SuppressionDescriptor _descriptor; + public DiagnosticSuppressorThrowsExceptionFromReportedSuppressions(string suppressedDiagnosticId) + { + _descriptor = new SuppressionDescriptor( + "SPR0001", + suppressedDiagnosticId, + $"Suppress {suppressedDiagnosticId}"); + } + + public override ImmutableArray SupportedSuppressions + => ImmutableArray.Create(_descriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + throw new NotImplementedException(); + } + } + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DiagnosticSuppressor_UnsupportedSuppressionReported : DiagnosticSuppressor + { + private readonly SuppressionDescriptor _supportedDescriptor; + private readonly SuppressionDescriptor _unsupportedDescriptor; + + public DiagnosticSuppressor_UnsupportedSuppressionReported(string suppressedDiagnosticId, string supportedSuppressionId, string unsupportedSuppressionId) + { + _supportedDescriptor = new SuppressionDescriptor( + supportedSuppressionId, + suppressedDiagnosticId, + $"Suppress {suppressedDiagnosticId}"); + + _unsupportedDescriptor = new SuppressionDescriptor( + unsupportedSuppressionId, + suppressedDiagnosticId, + $"Suppress {suppressedDiagnosticId}"); + } + + public override ImmutableArray SupportedSuppressions + => ImmutableArray.Create(_supportedDescriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + Assert.Equal(_unsupportedDescriptor.SuppressedDiagnosticId, diagnostic.Id); + context.ReportSuppression(Suppression.Create(_unsupportedDescriptor, diagnostic)); + } + } + } + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DiagnosticSuppressor_InvalidDiagnosticSuppressionReported : DiagnosticSuppressor + { + private readonly SuppressionDescriptor _supportedDescriptor; + private readonly SuppressionDescriptor _unsupportedDescriptor; + + public DiagnosticSuppressor_InvalidDiagnosticSuppressionReported(string suppressedDiagnosticId, string unsupportedSuppressedDiagnosticId) + { + _supportedDescriptor = new SuppressionDescriptor( + "SPR0001", + suppressedDiagnosticId, + $"Suppress {suppressedDiagnosticId}"); + + _unsupportedDescriptor = new SuppressionDescriptor( + "SPR0002", + unsupportedSuppressedDiagnosticId, + $"Suppress {unsupportedSuppressedDiagnosticId}"); + } + + public override ImmutableArray SupportedSuppressions + => ImmutableArray.Create(_supportedDescriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + Assert.Equal(_supportedDescriptor.SuppressedDiagnosticId, diagnostic.Id); + context.ReportSuppression(Suppression.Create(_unsupportedDescriptor, diagnostic)); + } + } + } + + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class DiagnosticSuppressor_NonReportedDiagnosticCannotBeSuppressed : DiagnosticSuppressor + { + private readonly SuppressionDescriptor _descriptor1, _descriptor2; + private readonly string _nonReportedDiagnosticId; + + public DiagnosticSuppressor_NonReportedDiagnosticCannotBeSuppressed(string reportedDiagnosticId, string nonReportedDiagnosticId) + { + _descriptor1 = new SuppressionDescriptor( + "SPR0001", + reportedDiagnosticId, + $"Suppress {reportedDiagnosticId}"); + _descriptor2 = new SuppressionDescriptor( + "SPR0002", + nonReportedDiagnosticId, + $"Suppress {nonReportedDiagnosticId}"); + _nonReportedDiagnosticId = nonReportedDiagnosticId; + } + + public override ImmutableArray SupportedSuppressions + => ImmutableArray.Create(_descriptor1, _descriptor2); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + var nonReportedDiagnostic = Diagnostic.Create( + id: _nonReportedDiagnosticId, + category: "Category", + message: "Message", + severity: DiagnosticSeverity.Warning, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + warningLevel: 1); + context.ReportSuppression(Suppression.Create(_descriptor2, nonReportedDiagnostic)); + } + } } } diff --git a/src/Test/Utilities/Portable/Diagnostics/DiagnosticDescription.cs b/src/Test/Utilities/Portable/Diagnostics/DiagnosticDescription.cs index 606dc3a9ba79a..476ff09454201 100644 --- a/src/Test/Utilities/Portable/Diagnostics/DiagnosticDescription.cs +++ b/src/Test/Utilities/Portable/Diagnostics/DiagnosticDescription.cs @@ -28,6 +28,8 @@ public sealed class DiagnosticDescription private readonly bool _argumentOrderDoesNotMatter; private readonly Type _errorCodeType; private readonly bool _ignoreArgumentsWhenComparing; + private readonly DiagnosticSeverity? _defaultSeverityOpt; + private readonly DiagnosticSeverity? _effectiveSeverityOpt; // fields for DiagnosticDescriptions constructed via factories private readonly Func _syntaxPredicate; @@ -64,7 +66,9 @@ public DiagnosticDescription( LinePosition? startLocation, Func syntaxNodePredicate, bool argumentOrderDoesNotMatter, - Type errorCodeType = null) + Type errorCodeType = null, + DiagnosticSeverity? defaultSeverityOpt = null, + DiagnosticSeverity? effectiveSeverityOpt = null) { _code = code; _isWarningAsError = isWarningAsError; @@ -74,6 +78,8 @@ public DiagnosticDescription( _syntaxPredicate = syntaxNodePredicate; _argumentOrderDoesNotMatter = argumentOrderDoesNotMatter; _errorCodeType = errorCodeType ?? code.GetType(); + _defaultSeverityOpt = defaultSeverityOpt; + _effectiveSeverityOpt = effectiveSeverityOpt; } public DiagnosticDescription( @@ -83,7 +89,9 @@ public DiagnosticDescription( LinePosition? startLocation, Func syntaxNodePredicate, bool argumentOrderDoesNotMatter, - Type errorCodeType = null) + Type errorCodeType = null, + DiagnosticSeverity? defaultSeverityOpt = null, + DiagnosticSeverity? effectiveSeverityOpt = null) { _code = code; _isWarningAsError = false; @@ -93,13 +101,17 @@ public DiagnosticDescription( _syntaxPredicate = syntaxNodePredicate; _argumentOrderDoesNotMatter = argumentOrderDoesNotMatter; _errorCodeType = errorCodeType ?? code.GetType(); + _defaultSeverityOpt = defaultSeverityOpt; + _effectiveSeverityOpt = effectiveSeverityOpt; } - public DiagnosticDescription(Diagnostic d, bool errorCodeOnly) + public DiagnosticDescription(Diagnostic d, bool errorCodeOnly, bool includeDefaultSeverity = false, bool includeEffectiveSeverity = false) { _code = d.Code; _isWarningAsError = d.IsWarningAsError; _location = d.Location; + _defaultSeverityOpt = includeDefaultSeverity ? d.DefaultSeverity : (DiagnosticSeverity?)null; + _effectiveSeverityOpt = includeEffectiveSeverity ? d.Severity : (DiagnosticSeverity?)null; DiagnosticWithInfo dinfo = null; if (d.Code == 0) @@ -160,17 +172,27 @@ public DiagnosticDescription(Diagnostic d, bool errorCodeOnly) public DiagnosticDescription WithArguments(params string[] arguments) { - return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, arguments, _startPosition, _syntaxPredicate, false, _errorCodeType); + return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, arguments, _startPosition, _syntaxPredicate, false, _errorCodeType, _defaultSeverityOpt, _effectiveSeverityOpt); } public DiagnosticDescription WithArgumentsAnyOrder(params string[] arguments) { - return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, arguments, _startPosition, _syntaxPredicate, true, _errorCodeType); + return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, arguments, _startPosition, _syntaxPredicate, true, _errorCodeType, _defaultSeverityOpt, _effectiveSeverityOpt); } public DiagnosticDescription WithWarningAsError(bool isWarningAsError) { - return new DiagnosticDescription(_code, isWarningAsError, _squiggledText, _arguments, _startPosition, _syntaxPredicate, true, _errorCodeType); + return new DiagnosticDescription(_code, isWarningAsError, _squiggledText, _arguments, _startPosition, _syntaxPredicate, true, _errorCodeType, _defaultSeverityOpt, _effectiveSeverityOpt); + } + + public DiagnosticDescription WithDefaultSeverity(DiagnosticSeverity defaultSeverity) + { + return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, _arguments, _startPosition, _syntaxPredicate, true, _errorCodeType, defaultSeverity, _effectiveSeverityOpt); + } + + public DiagnosticDescription WithEffectiveSeverity(DiagnosticSeverity effectiveSeverity) + { + return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, _arguments, _startPosition, _syntaxPredicate, true, _errorCodeType, _defaultSeverityOpt, effectiveSeverity); } /// @@ -178,7 +200,7 @@ public DiagnosticDescription WithWarningAsError(bool isWarningAsError) /// public DiagnosticDescription WithLocation(int line, int column) { - return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, _arguments, new LinePosition(line - 1, column - 1), _syntaxPredicate, _argumentOrderDoesNotMatter, _errorCodeType); + return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, _arguments, new LinePosition(line - 1, column - 1), _syntaxPredicate, _argumentOrderDoesNotMatter, _errorCodeType, _defaultSeverityOpt, _effectiveSeverityOpt); } /// @@ -187,11 +209,14 @@ public DiagnosticDescription WithLocation(int line, int column) /// The argument to syntaxPredicate will be the nearest SyntaxNode whose Span contains first squiggled character. public DiagnosticDescription WhereSyntax(Func syntaxPredicate) { - return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, _arguments, _startPosition, syntaxPredicate, _argumentOrderDoesNotMatter, _errorCodeType); + return new DiagnosticDescription(_code, _isWarningAsError, _squiggledText, _arguments, _startPosition, syntaxPredicate, _argumentOrderDoesNotMatter, _errorCodeType, _defaultSeverityOpt, _effectiveSeverityOpt); } public object Code => _code; public bool HasLocation => _startPosition != null; + public bool IsWarningAsError => _isWarningAsError; + public DiagnosticSeverity? DefaultSeverity => _defaultSeverityOpt; + public DiagnosticSeverity? EffectiveSeverity => _effectiveSeverityOpt; public override bool Equals(object obj) { @@ -280,6 +305,12 @@ public override bool Equals(object obj) } } + if (_defaultSeverityOpt != d._defaultSeverityOpt || + _effectiveSeverityOpt != d._effectiveSeverityOpt) + { + return false; + } + return true; } @@ -294,6 +325,10 @@ public override int GetHashCode() hashCode = Hash.Combine(_arguments, hashCode); if (_startPosition != null) hashCode = Hash.Combine(hashCode, _startPosition.Value.GetHashCode()); + if (_defaultSeverityOpt != null) + hashCode = Hash.Combine(hashCode, _defaultSeverityOpt.Value.GetHashCode()); + if (_effectiveSeverityOpt != null) + hashCode = Hash.Combine(hashCode, _effectiveSeverityOpt.Value.GetHashCode()); return hashCode; } @@ -362,6 +397,16 @@ public override string ToString() sb.Append(".WithWarningAsError(true)"); } + if (_defaultSeverityOpt != null) + { + sb.Append($".WithDefaultSeverity(DiagnosticSeverity.{_defaultSeverityOpt.Value.ToString()})"); + } + + if (_effectiveSeverityOpt != null) + { + sb.Append($".WithEffectiveSeverity(DiagnosticSeverity.{_effectiveSeverityOpt.Value.ToString()})"); + } + if (_syntaxPredicate != null && _showPredicate) { sb.Append(".WhereSyntax(...)"); @@ -377,6 +422,8 @@ public static string GetAssertText(DiagnosticDescription[] expected, IEnumerable var language = actual.Any() && actual.First().Id.StartsWith("CS", StringComparison.Ordinal) ? CSharp : VisualBasic; var includeDiagnosticMessagesAsComments = (language == CSharp); int indentDepth = (language == CSharp) ? 4 : 1; + var includeDefaultSeverity = expected.Any() && expected.All(d => d.DefaultSeverity != null); + var includeEffectiveSeverity = expected.Any() && expected.All(d => d.EffectiveSeverity != null); if (IsSortedOrEmpty(expected)) { @@ -430,7 +477,7 @@ public static string GetAssertText(DiagnosticDescription[] expected, IEnumerable } } - var description = new DiagnosticDescription(d, errorCodeOnly: false); + var description = new DiagnosticDescription(d, errorCodeOnly: false, includeDefaultSeverity, includeEffectiveSeverity); var diffDescription = description; var idx = Array.IndexOf(expected, description); if (idx != -1) diff --git a/src/Test/Utilities/Portable/Diagnostics/DiagnosticExtensions.cs b/src/Test/Utilities/Portable/Diagnostics/DiagnosticExtensions.cs index d447ba455b0ee..64cf035347e30 100644 --- a/src/Test/Utilities/Portable/Diagnostics/DiagnosticExtensions.cs +++ b/src/Test/Utilities/Portable/Diagnostics/DiagnosticExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Emit; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Roslyn.Utilities; @@ -69,7 +70,10 @@ private static void Verify(IEnumerable actual, DiagnosticDescription throw new ArgumentException("Must specify expected errors.", nameof(expected)); } - var unmatched = actual.Select(d => new DiagnosticDescription(d, errorCodeOnly)).ToList(); + var includeDefaultSeverity = expected.Any() && expected.All(e => e.DefaultSeverity != null); + var includeEffectiveSeverity = expected.Any() && expected.All(e => e.EffectiveSeverity != null); + var unmatched = actual.Select(d => new DiagnosticDescription(d, errorCodeOnly, includeDefaultSeverity, includeEffectiveSeverity)) + .ToList(); // Try to match each of the 'expected' errors to one of the 'actual' ones. // If any of the expected errors don't appear, fail test. @@ -162,7 +166,9 @@ public static TCompilation VerifyAnalyzerDiagnostics( params DiagnosticDescription[] expected) where TCompilation : Compilation { - c = c.GetAnalyzerDiagnostics(analyzers, options, onAnalyzerException, logAnalyzerExceptionAsDiagnostics, reportSuppressedDiagnostics, diagnostics: out var diagnostics); + c = c.GetCompilationWithAnalyzerDiagnostics(analyzers, options, onAnalyzerException, + logAnalyzerExceptionAsDiagnostics, reportSuppressedDiagnostics, + includeCompilerDiagnostics: false, diagnostics: out var diagnostics); diagnostics.Verify(expected); return c; // note this is a new compilation } @@ -187,18 +193,107 @@ public static ImmutableArray GetAnalyzerDiagnostics( bool logAnalyzerExceptionAsDiagnostics = true) where TCompilation : Compilation { - c = GetAnalyzerDiagnostics(c, analyzers, options, onAnalyzerException, logAnalyzerExceptionAsDiagnostics, reportSuppressedDiagnostics, out var diagnostics); + c = GetCompilationWithAnalyzerDiagnostics(c, analyzers, options, onAnalyzerException, + logAnalyzerExceptionAsDiagnostics, reportSuppressedDiagnostics, + includeCompilerDiagnostics: false, out var diagnostics); return diagnostics; } - private static TCompilation GetAnalyzerDiagnostics( - this TCompilation c, - DiagnosticAnalyzer[] analyzers, - AnalyzerOptions options, - Action onAnalyzerException, - bool logAnalyzerExceptionAsDiagnostics, - bool reportSuppressedDiagnostics, - out ImmutableArray diagnostics) + public static TCompilation VerifySuppressedDiagnostics( + this TCompilation c, + DiagnosticAnalyzer[] analyzers, + AnalyzerOptions options = null, + Action onAnalyzerException = null, + bool logAnalyzerExceptionAsDiagnostics = true, + params DiagnosticDescription[] expected) + where TCompilation : Compilation + { + // Verify suppression is unaffected by toggling /warnaserror. + // Only perform this additional verification if the caller hasn't + // explicitly overridden specific or general diagnostic options. + if (c.Options.GeneralDiagnosticOption == ReportDiagnostic.Default && + c.Options.SpecificDiagnosticOptions.IsEmpty) + { + _ = c.VerifySuppressedDiagnostics(toggleWarnAsError: true, + analyzers, options, onAnalyzerException, logAnalyzerExceptionAsDiagnostics, + expected); + } + + return c.VerifySuppressedDiagnostics(toggleWarnAsError: false, + analyzers, options, onAnalyzerException, logAnalyzerExceptionAsDiagnostics, + expected); + } + + private static TCompilation VerifySuppressedDiagnostics( + this TCompilation c, + bool toggleWarnAsError, + DiagnosticAnalyzer[] analyzers, + AnalyzerOptions options, + Action onAnalyzerException, + bool logAnalyzerExceptionAsDiagnostics, + DiagnosticDescription[] expectedDiagnostics) + where TCompilation : Compilation + { + if (toggleWarnAsError) + { + var toggledOption = c.Options.GeneralDiagnosticOption == ReportDiagnostic.Error ? + ReportDiagnostic.Default : + ReportDiagnostic.Error; + c = (TCompilation)c.WithOptions(c.Options.WithGeneralDiagnosticOption(toggledOption)); + + var builder = ArrayBuilder.GetInstance(expectedDiagnostics.Length); + foreach (var expected in expectedDiagnostics) + { + // Toggle warnaserror and effective severity if following are true: + // 1. Default severity is not specified or specified as Warning + // 2. Effective severity is not specified or specified as Warning or Error + var defaultSeverityCheck = !expected.DefaultSeverity.HasValue || + expected.DefaultSeverity.Value == DiagnosticSeverity.Warning; + var effectiveSeverityCheck = !expected.EffectiveSeverity.HasValue || + expected.EffectiveSeverity.Value == DiagnosticSeverity.Warning || + expected.EffectiveSeverity.Value == DiagnosticSeverity.Error; + + DiagnosticDescription newExpected; + if (defaultSeverityCheck && effectiveSeverityCheck) + { + newExpected = expected.WithWarningAsError(!expected.IsWarningAsError); + + if (expected.EffectiveSeverity.HasValue) + { + var newEffectiveSeverity = expected.EffectiveSeverity.Value == DiagnosticSeverity.Error ? + DiagnosticSeverity.Warning : + DiagnosticSeverity.Error; + newExpected = newExpected.WithEffectiveSeverity(newEffectiveSeverity); + } + } + else + { + newExpected = expected; + } + + builder.Add(newExpected); + } + + expectedDiagnostics = builder.ToArrayAndFree(); + } + + c = c.GetCompilationWithAnalyzerDiagnostics(analyzers, options, onAnalyzerException, + logAnalyzerExceptionAsDiagnostics, reportSuppressedDiagnostics: true, + includeCompilerDiagnostics: true, diagnostics: out var diagnostics); + diagnostics = diagnostics.WhereAsArray(d => d.IsSuppressed); + diagnostics.Verify(expectedDiagnostics); + return c; // note this is a new compilation + } + + private static TCompilation GetCompilationWithAnalyzerDiagnostics( + this TCompilation c, + DiagnosticAnalyzer[] analyzers, + AnalyzerOptions options, + Action onAnalyzerException, + bool logAnalyzerExceptionAsDiagnostics, + bool reportSuppressedDiagnostics, + bool includeCompilerDiagnostics, + out ImmutableArray diagnostics) where TCompilation : Compilation { var analyzersArray = analyzers.ToImmutableArray(); @@ -228,8 +323,13 @@ private static TCompilation GetAnalyzerDiagnostics( var analyzerManager = new AnalyzerManager(analyzersArray); var driver = AnalyzerDriver.CreateAndAttachToCompilation(c, analyzersArray, options, analyzerManager, onAnalyzerException, null, false, out var newCompilation, CancellationToken.None); - var discarded = newCompilation.GetDiagnostics(); - diagnostics = driver.GetDiagnosticsAsync(newCompilation).Result.AddRange(exceptionDiagnostics); + + var compilerDiagnostics = newCompilation.GetDiagnostics(); + var analyzerDiagnostics = driver.GetDiagnosticsAsync(newCompilation).Result; + var allDiagnostics = includeCompilerDiagnostics ? + compilerDiagnostics.AddRange(analyzerDiagnostics) : + analyzerDiagnostics; + diagnostics = driver.ApplyProgrammaticSuppressions(allDiagnostics, newCompilation).AddRange(exceptionDiagnostics); if (!reportSuppressedDiagnostics) { diff --git a/src/Tools/AnalyzerRunner/DiagnosticAnalyzerRunner.cs b/src/Tools/AnalyzerRunner/DiagnosticAnalyzerRunner.cs index 806c4de6b54e5..fc2891ec7e4b0 100644 --- a/src/Tools/AnalyzerRunner/DiagnosticAnalyzerRunner.cs +++ b/src/Tools/AnalyzerRunner/DiagnosticAnalyzerRunner.cs @@ -427,6 +427,8 @@ private static void WriteTelemetry(string analyzerName, AnalyzerTelemetryInfo te WriteLine($"Symbol End Actions: {telemetry.SymbolEndActionsCount}", ConsoleColor.White); WriteLine($"Syntax Node Actions: {telemetry.SyntaxNodeActionsCount}", ConsoleColor.White); WriteLine($"Syntax Tree Actions: {telemetry.SyntaxTreeActionsCount}", ConsoleColor.White); + + WriteLine($"Suppression Actions: {telemetry.SuppressionActionsCount}", ConsoleColor.White); } private static void WriteExecutionTimes(string analyzerName, int longestAnalyzerName, AnalyzerTelemetryInfo telemetry) diff --git a/src/Tools/AnalyzerRunner/Extensions.cs b/src/Tools/AnalyzerRunner/Extensions.cs index f92600ae181fb..e721025c051c9 100644 --- a/src/Tools/AnalyzerRunner/Extensions.cs +++ b/src/Tools/AnalyzerRunner/Extensions.cs @@ -24,6 +24,7 @@ internal static void Add(this AnalyzerTelemetryInfo analyzerTelemetryInfo, Analy analyzerTelemetryInfo.SymbolEndActionsCount += addendum.SymbolEndActionsCount; analyzerTelemetryInfo.SyntaxNodeActionsCount += addendum.SyntaxNodeActionsCount; analyzerTelemetryInfo.SyntaxTreeActionsCount += addendum.SyntaxTreeActionsCount; + analyzerTelemetryInfo.SuppressionActionsCount += addendum.SuppressionActionsCount; } } }