|
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using ILLink.Shared;
using Mono.Cecil;
namespace Mono.Linker
{
public class UnconditionalSuppressMessageAttributeState
{
internal const string ScopeProperty = "Scope";
internal const string TargetProperty = "Target";
internal const string MessageIdProperty = "MessageId";
public class Suppression
{
public SuppressMessageInfo SuppressMessageInfo { get; }
public bool Used { get; set; }
public CustomAttribute OriginAttribute { get; }
public ICustomAttributeProvider Provider { get; }
public Suppression (SuppressMessageInfo suppressMessageInfo, CustomAttribute originAttribute, ICustomAttributeProvider provider)
{
SuppressMessageInfo = suppressMessageInfo;
OriginAttribute = originAttribute;
Provider = provider;
}
}
readonly LinkContext _context;
readonly Dictionary<ICustomAttributeProvider, Dictionary<int, Suppression>> _suppressions;
HashSet<AssemblyDefinition> InitializedAssemblies { get; }
public UnconditionalSuppressMessageAttributeState (LinkContext context)
{
_context = context;
_suppressions = new Dictionary<ICustomAttributeProvider, Dictionary<int, Suppression>> ();
InitializedAssemblies = new HashSet<AssemblyDefinition> ();
}
void AddSuppression (Suppression suppression)
{
var used = false;
if (!_suppressions.TryGetValue (suppression.Provider, out var suppressions)) {
suppressions = new Dictionary<int, Suppression> ();
_suppressions.Add (suppression.Provider, suppressions);
} else if (suppressions.TryGetValue (suppression.SuppressMessageInfo.Id, out Suppression? value)) {
used = value.Used;
string? elementName = suppression.Provider is MemberReference memberRef ? memberRef.GetDisplayName () : suppression.Provider.ToString ();
_context.LogMessage ($"Element '{elementName}' has more than one unconditional suppression. Note that only the last one is used.");
}
suppression.Used = used;
suppressions[suppression.SuppressMessageInfo.Id] = suppression;
}
public bool IsSuppressed (int id, MessageOrigin warningOrigin, out SuppressMessageInfo info)
{
// Check for suppressions on both the suppression context as well as the original member
// (if they're different). This is to correctly handle compiler generated code
// which needs to use suppressions from both the compiler generated scope
// as well as the original user defined method.
info = default;
ICustomAttributeProvider? provider = warningOrigin.Provider;
if (provider == null)
return false;
if (IsSuppressed (id, provider, out info))
return true;
if (provider is not IMemberDefinition member)
return false;
MethodDefinition? owningMethod;
while (_context.CompilerGeneratedState.TryGetOwningMethodForCompilerGeneratedMember (member, out owningMethod)) {
Debug.Assert (owningMethod != member);
if (IsSuppressed (id, owningMethod, out info))
return true;
member = owningMethod;
}
return false;
}
public void GatherSuppressions (ICustomAttributeProvider provider)
{
TryGetSuppressionsForProvider (provider, out _);
}
public IEnumerable<Suppression> GetUnusedSuppressions ()
{
foreach (var (provider, suppressions) in _suppressions) {
foreach (var (_, suppression) in suppressions) {
if (!suppression.Used)
yield return suppression;
}
}
}
bool IsSuppressed (int id, ICustomAttributeProvider warningOrigin, out SuppressMessageInfo info)
{
info = default;
if (warningOrigin is IMemberDefinition warningOriginMember) {
while (warningOriginMember != null) {
if (IsSuppressedOnElement (id, warningOriginMember, out info))
return true;
if (warningOriginMember is MethodDefinition method) {
if (method.TryGetProperty (out var property)) {
Debug.Assert (property.DeclaringType == warningOriginMember.DeclaringType);
warningOriginMember = property;
continue;
} else if (method.TryGetEvent (out var @event)) {
Debug.Assert (@event.DeclaringType == warningOriginMember.DeclaringType);
warningOriginMember = @event;
continue;
}
}
warningOriginMember = warningOriginMember.DeclaringType;
}
}
ModuleDefinition? module = GetModuleFromProvider (warningOrigin);
if (module == null)
return false;
// Check if there's an assembly or module level suppression.
if (IsSuppressedOnElement (id, module, out info) ||
IsSuppressedOnElement (id, module.Assembly, out info))
return true;
return false;
}
bool IsSuppressedOnElement (int id, ICustomAttributeProvider? provider, out SuppressMessageInfo info)
{
info = default;
if (TryGetSuppressionsForProvider (provider, out var suppressions)) {
if (suppressions != null && suppressions.TryGetValue (id, out var suppression)) {
suppression.Used = true;
info = suppression.SuppressMessageInfo;
return true;
}
}
return false;
}
bool TryGetSuppressionsForProvider (ICustomAttributeProvider? provider, out Dictionary<int, Suppression>? suppressions)
{
suppressions = null;
if (provider == null)
return false;
if (_suppressions.TryGetValue (provider, out suppressions))
return true;
// Populate the cache with suppressions for this member. We need to look for suppressions on the
// member itself, and on the assembly/module.
var membersToScan = new HashSet<ICustomAttributeProvider> { { provider } };
// Gather assembly-level suppressions if we haven't already. To ensure that we always cache
// complete information for a member, we will also scan for attributes on any other members
// targeted by the assembly-level suppressions.
if (GetModuleFromProvider (provider) is ModuleDefinition module) {
var assembly = module.Assembly;
if (InitializedAssemblies.Add (assembly)) {
foreach (var suppression in DecodeAssemblyAndModuleSuppressions (module)) {
AddSuppression (suppression);
membersToScan.Add (suppression.Provider);
}
}
}
// Populate the cache for this member, and for any members that were targeted by assembly-level
// suppressions to make sure the cached info is complete.
foreach (var member in membersToScan) {
if (member is ModuleDefinition or AssemblyDefinition)
continue;
foreach (var suppression in DecodeSuppressions (member))
AddSuppression (suppression);
}
return _suppressions.TryGetValue (provider, out suppressions);
}
static bool TryDecodeSuppressMessageAttributeData (CustomAttribute attribute, out SuppressMessageInfo info)
{
info = default;
// We need at least the Category and Id to decode the warning to suppress.
// The only UnconditionalSuppressMessageAttribute constructor requires those two parameters.
if (attribute.ConstructorArguments.Count < 2) {
return false;
}
// Ignore the category parameter because it does not identify the warning
// and category information can be obtained from warnings themselves.
// We only support warnings with code pattern IL####.
if (!(attribute.ConstructorArguments[1].Value is string warningId) ||
warningId.Length < 6 ||
!warningId.StartsWith ("IL") ||
!int.TryParse (warningId.AsSpan (2, 4), out info.Id)) {
return false;
}
if (warningId.Length > 6 && warningId[6] != ':')
return false;
if (attribute.HasProperties) {
foreach (var p in attribute.Properties) {
switch (p.Name) {
case ScopeProperty when p.Argument.Value is string scope:
info.Scope = scope;
break;
case TargetProperty when p.Argument.Value is string target:
info.Target = target;
break;
case MessageIdProperty when p.Argument.Value is string messageId:
info.MessageId = messageId;
break;
}
}
}
return true;
}
public static ModuleDefinition? GetModuleFromProvider (ICustomAttributeProvider provider)
{
switch (provider.MetadataToken.TokenType) {
case TokenType.Module:
return provider as ModuleDefinition;
case TokenType.Assembly:
return ((AssemblyDefinition) provider).MainModule;
case TokenType.TypeDef:
return ((TypeDefinition) provider).Module;
case TokenType.Method:
case TokenType.Property:
case TokenType.Field:
case TokenType.Event:
return ((IMemberDefinition) provider).DeclaringType.Module;
default:
return null;
}
}
IEnumerable<Suppression> DecodeSuppressions (ICustomAttributeProvider provider)
{
Debug.Assert (provider is not (ModuleDefinition or AssemblyDefinition));
if (!_context.CustomAttributes.HasAny (provider))
yield break;
foreach (var ca in _context.CustomAttributes.GetCustomAttributes (provider)) {
if (!TypeRefHasUnconditionalSuppressions (ca.Constructor.DeclaringType))
continue;
if (!TryDecodeSuppressMessageAttributeData (ca, out var info))
continue;
yield return new Suppression (info, originAttribute: ca, provider);
}
}
IEnumerable<Suppression> DecodeAssemblyAndModuleSuppressions (ModuleDefinition module)
{
AssemblyDefinition assembly = module.Assembly;
foreach (var suppression in DecodeGlobalSuppressions (module, assembly))
yield return suppression;
foreach (var _module in assembly.Modules) {
foreach (var suppression in DecodeGlobalSuppressions (_module, _module))
yield return suppression;
}
}
IEnumerable<Suppression> DecodeGlobalSuppressions (ModuleDefinition module, ICustomAttributeProvider provider)
{
var attributes = _context.CustomAttributes.GetCustomAttributes (provider).
Where (a => TypeRefHasUnconditionalSuppressions (a.AttributeType));
foreach (var instance in attributes) {
SuppressMessageInfo info;
if (!TryDecodeSuppressMessageAttributeData (instance, out info))
continue;
var scope = info.Scope?.ToLowerInvariant ();
if (info.Target == null && (scope == "module" || scope == null)) {
yield return new Suppression (info, originAttribute: instance, provider);
continue;
}
switch (scope) {
case "module":
yield return new Suppression (info, originAttribute: instance, provider);
break;
case "type":
case "member":
if (info.Target == null)
break;
foreach (var result in DocumentationSignatureParser.GetMembersForDocumentationSignature (info.Target, module, _context))
yield return new Suppression (info, originAttribute: instance, result);
break;
default:
_context.LogWarning (_context.GetAssemblyLocation (module.Assembly), DiagnosticId.InvalidScopeInUnconditionalSuppressMessage, info.Scope ?? "", module.Name, info.Target ?? "");
break;
}
}
}
static bool TypeRefHasUnconditionalSuppressions (TypeReference typeRef)
{
return typeRef.Name == "UnconditionalSuppressMessageAttribute" &&
typeRef.Namespace == "System.Diagnostics.CodeAnalysis";
}
public MessageOrigin GetSuppressionOrigin (Suppression suppression)
{
if (_context.CustomAttributes.TryGetCustomAttributeOrigin (suppression.Provider, suppression.OriginAttribute, out MessageOrigin origin))
return origin;
if (suppression.Provider is ModuleDefinition module)
return new MessageOrigin (module.Assembly);
return new MessageOrigin (suppression.Provider);
}
}
}
|