diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/SuppressMessageAttributeState.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/SuppressMessageAttributeState.cs index e0355be6fd319f11426749f20817ffeeea9b5632..f6c5786a25b933dfd465c201ac990e71d57e88a1 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/SuppressMessageAttributeState.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/SuppressMessageAttributeState.cs @@ -1,5 +1,6 @@ // 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.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -12,16 +13,20 @@ namespace Microsoft.CodeAnalysis.Diagnostics { internal partial class SuppressMessageAttributeState { - private static readonly SmallDictionary s_suppressMessageScopeTypes = new SmallDictionary() + private static readonly SmallDictionary s_suppressMessageScopeTypes = new SmallDictionary(StringComparer.OrdinalIgnoreCase) { - { null, TargetScope.None }, + { string.Empty, TargetScope.None }, { "module", TargetScope.Module }, { "namespace", TargetScope.Namespace }, { "resource", TargetScope.Resource }, { "type", TargetScope.Type }, - { "member", TargetScope.Member } + { "member", TargetScope.Member }, + { "namespaceandchildren", TargetScope.NamespaceAndChildren } }; + private static bool TryGetTargetScope(SuppressMessageInfo info, out TargetScope scope) + => s_suppressMessageScopeTypes.TryGetValue(info.Scope ?? string.Empty, out scope); + private readonly Compilation _compilation; private GlobalSuppressions _lazyGlobalSuppressions; private readonly ConcurrentDictionary> _localSuppressionsBySymbol; @@ -56,14 +61,31 @@ public bool HasCompilationWideSuppression(string id, out SuppressMessageInfo inf return _compilationWideSuppressions.TryGetValue(id, out info); } - public bool HasGlobalSymbolSuppression(ISymbol symbol, string id, out SuppressMessageInfo info) + public bool HasGlobalSymbolSuppression(ISymbol symbol, string id, bool isImmediatelyContainingSymbol, out SuppressMessageInfo info) { Debug.Assert(symbol != null); Dictionary suppressions; if (_globalSymbolSuppressions.TryGetValue(symbol, out suppressions) && suppressions.TryGetValue(id, out info)) { - return true; + if (symbol.Kind != SymbolKind.Namespace) + { + return true; + } + + if (TryGetTargetScope(info, out TargetScope targetScope)) + { + switch (targetScope) + { + case TargetScope.Namespace: + // Special case: Only suppress syntax diagnostics in namespace declarations if the namespace is the closest containing symbol. + // In other words, only apply suppression to the immediately containing namespace declaration and not to its children or parents. + return isImmediatelyContainingSymbol; + + case TargetScope.NamespaceAndChildren: + return true; + } + } } info = default(SuppressMessageInfo); @@ -118,7 +140,7 @@ private bool IsDiagnosticSuppressed(string id, Location location, out SuppressMe info = default(SuppressMessageInfo); - if (IsDiagnosticGloballySuppressed(id, symbolOpt: null, info: out info)) + if (IsDiagnosticGloballySuppressed(id, symbolOpt: null, isImmediatelyContainingSymbol: false, info: out info)) { return true; } @@ -140,28 +162,46 @@ private bool IsDiagnosticSuppressed(string id, Location location, out SuppressMe { if (symbol.Kind == SymbolKind.Namespace) { - // Special case: Only suppress syntax diagnostics in namespace declarations if the namespace is the closest containing symbol. - // In other words, only apply suppression to the immediately containing namespace declaration and not to its children or parents. - return inImmediatelyContainingSymbol && IsDiagnosticGloballySuppressed(id, symbol, out info); + return hasNamespaceSuppression((INamespaceSymbol)symbol, inImmediatelyContainingSymbol); } - else if (IsDiagnosticLocallySuppressed(id, symbol, out info) || IsDiagnosticGloballySuppressed(id, symbol, out info)) + else if (IsDiagnosticLocallySuppressed(id, symbol, out info) || IsDiagnosticGloballySuppressed(id, symbol, inImmediatelyContainingSymbol, out info)) { return true; } + } + if (!declaredSymbols.IsEmpty) + { inImmediatelyContainingSymbol = false; } } } return false; + + bool hasNamespaceSuppression(INamespaceSymbol namespaceSymbol, bool inImmediatelyContainingSymbol) + { + do + { + if (IsDiagnosticGloballySuppressed(id, namespaceSymbol, inImmediatelyContainingSymbol, out _)) + { + return true; + } + + namespaceSymbol = namespaceSymbol.ContainingNamespace; + inImmediatelyContainingSymbol = false; + } + while (namespaceSymbol != null); + + return false; + } } - private bool IsDiagnosticGloballySuppressed(string id, ISymbol symbolOpt, out SuppressMessageInfo info) + private bool IsDiagnosticGloballySuppressed(string id, ISymbol symbolOpt, bool isImmediatelyContainingSymbol, out SuppressMessageInfo info) { this.DecodeGlobalSuppressMessageAttributes(); return _lazyGlobalSuppressions.HasCompilationWideSuppression(id, out info) || - symbolOpt != null && _lazyGlobalSuppressions.HasGlobalSymbolSuppression(symbolOpt, id, out info); + symbolOpt != null && _lazyGlobalSuppressions.HasGlobalSymbolSuppression(symbolOpt, id, isImmediatelyContainingSymbol, out info); } private bool IsDiagnosticLocallySuppressed(string id, ISymbol symbol, out SuppressMessageInfo info) @@ -251,10 +291,7 @@ private static void DecodeGlobalSuppressMessageAttributes(Compilation compilatio continue; } - string scopeString = info.Scope != null ? info.Scope.ToLowerInvariant() : null; - TargetScope scope; - - if (s_suppressMessageScopeTypes.TryGetValue(scopeString, out scope)) + if (TryGetTargetScope(info, out TargetScope scope)) { if ((scope == TargetScope.Module || scope == TargetScope.None) && info.Target == null) { @@ -294,6 +331,10 @@ internal static IEnumerable ResolveTargetSymbols(Compilation compilatio new TargetSymbolResolver(compilation, scope, target).Resolve(results); return results; } + + case TargetScope.NamespaceAndChildren: + return ResolveTargetSymbols(compilation, target, TargetScope.Namespace); + default: return SpecializedCollections.EmptyEnumerable(); } @@ -341,7 +382,8 @@ internal enum TargetScope Namespace, Resource, Type, - Member + Member, + NamespaceAndChildren } } } diff --git a/src/Test/Utilities/Portable/Diagnostics/SuppressMessageAttributeTests.cs b/src/Test/Utilities/Portable/Diagnostics/SuppressMessageAttributeTests.cs index 5bfeb0c91b9193cf4a042419c756cfde4549ee12..c134ec7a6c860fc8505903cc6b8373b79465ff5b 100644 --- a/src/Test/Utilities/Portable/Diagnostics/SuppressMessageAttributeTests.cs +++ b/src/Test/Utilities/Portable/Diagnostics/SuppressMessageAttributeTests.cs @@ -110,6 +110,101 @@ namespace N4 Diagnostic("Declaration", "N3")); } + [Fact, WorkItem(486, "https://github.com/dotnet/roslyn/issues/486")] + public async Task GlobalSuppressionOnNamespaces_NamespaceAndChildren() + { + await VerifyCSharpAsync(@" +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage(""Test"", ""Declaration"", Scope=""NamespaceAndChildren"", Target=""N.N1"")] +[module: SuppressMessage(""Test"", ""Declaration"", Scope=""namespaceandchildren"", Target=""N4"")] + +namespace N +{ + namespace N1 + { + namespace N2.N3 + { + } + } +} + +namespace N4 +{ + namespace N5 + { + } +} + +namespace N.N1.N6.N7 +{ +} +", + new[] { new WarningOnNamePrefixDeclarationAnalyzer("N") }, + Diagnostic("Declaration", "N")); + } + + [Fact, WorkItem(486, "https://github.com/dotnet/roslyn/issues/486")] + public async Task GlobalSuppressionOnTypesAndNamespaces_NamespaceAndChildren() + { + await VerifyCSharpAsync(@" +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage(""Test"", ""Declaration"", Scope=""NamespaceAndChildren"", Target=""N.N1.N2"")] +[module: SuppressMessage(""Test"", ""Declaration"", Scope=""NamespaceAndChildren"", Target=""N4"")] +[module: SuppressMessage(""Test"", ""Declaration"", Scope=""Type"", Target=""C2"")] + +namespace N +{ + namespace N1 + { + class C1 + { + } + + namespace N2.N3 + { + class C2 + { + } + + class C3 + { + class C4 + { + } + } + } + } +} + +namespace N4 +{ + namespace N5 + { + class C5 + { + } + } + + class C6 + { + } +} + +namespace N.N1.N2.N7 +{ + class C7 + { + } +} +", + new[] { new WarningOnNamePrefixDeclarationAnalyzer("N"), new WarningOnNamePrefixDeclarationAnalyzer("C") }, + Diagnostic("Declaration", "N"), + Diagnostic("Declaration", "N1"), + Diagnostic("Declaration", "C1")); + } + [Fact] public async Task GlobalSuppressionOnTypes() { @@ -350,6 +445,23 @@ class C {} Diagnostic("Token", "}").WithLocation(9, 1)); } + [Fact, WorkItem(486, "https://github.com/dotnet/roslyn/issues/486")] + public async Task SuppressSyntaxDiagnosticsOnNamespaceAndChildDeclarationCSharp() + { + await VerifyTokenDiagnosticsCSharpAsync(@" +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage(""Test"", ""Token"", Scope=""NamespaceAndChildren"", Target=""A.B"")] +namespace A +[|{ + namespace B + { + class C {} + } +}|] +", + Diagnostic("Token", "{").WithLocation(4, 1), + Diagnostic("Token", "}").WithLocation(9, 1)); + } + [Fact] public async Task SuppressSyntaxDiagnosticsOnNamespaceDeclarationBasic() { @@ -370,11 +482,29 @@ End Namespace Diagnostic("Token", "End").WithLocation(8, 1)); } - [Fact] - public async Task DontSuppressSyntaxDiagnosticsInRootNamespaceBasic() + [Fact, WorkItem(486, "https://github.com/dotnet/roslyn/issues/486")] + public async Task SuppressSyntaxDiagnosticsOnNamespaceAndChildrenDeclarationBasic() { - await VerifyBasicAsync(@" - + await VerifyTokenDiagnosticsBasicAsync(@" + +Namespace [|A + Namespace B + Class C + End Class + End Namespace +End|] Namespace +", + Diagnostic("Token", "A").WithLocation(3, 11), + Diagnostic("Token", "End").WithLocation(8, 1)); + } + + [Theory, WorkItem(486, "https://github.com/dotnet/roslyn/issues/486")] + [InlineData("Namespace")] + [InlineData("NamespaceAndChildren")] + public async Task DontSuppressSyntaxDiagnosticsInRootNamespaceBasic(string scope) + { + await VerifyBasicAsync($@" + ' In root namespace ", rootNamespace: "RootNamespace",