File: Compiler\Logging\UnconditionalSuppressMessageAttributeState.cs
Web Access
Project: src\src\runtime\src\coreclr\tools\aot\ILCompiler.Compiler\ILCompiler.Compiler.csproj (ILCompiler.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;

using Internal.TypeSystem;
using Internal.TypeSystem.Ecma;

using ILCompiler.Dataflow;

using ILLink.Shared;

#nullable enable

namespace ILCompiler.Logging
{
    public class UnconditionalSuppressMessageAttributeState
    {
        internal const string ScopeProperty = "Scope";
        internal const string TargetProperty = "Target";
        internal const string MessageIdProperty = "MessageId";

        internal const string UnconditionalSuppressMessageAttributeNamespace = "System.Diagnostics.CodeAnalysis";
        internal const string UnconditionalSuppressMessageAttributeName = "UnconditionalSuppressMessageAttribute";

        public class Suppression
        {
            public SuppressMessageInfo SuppressMessageInfo { get; }
            public bool Used { get; set; }
            public CustomAttributeValue<TypeDesc> OriginAttribute { get; }
            public TypeSystemEntity Provider { get; }

            public Suppression(SuppressMessageInfo suppressMessageInfo, CustomAttributeValue<TypeDesc> originAttribute, TypeSystemEntity provider)
            {
                SuppressMessageInfo = suppressMessageInfo;
                OriginAttribute = originAttribute;
                Provider = provider;
            }
        }

        private sealed class AssemblyWarningsReportedHashtable : LockFreeReaderHashtable<EcmaAssembly, EcmaAssembly>
        {
            protected override bool CompareKeyToValue(EcmaAssembly key, EcmaAssembly value) => key == value;
            protected override bool CompareValueToValue(EcmaAssembly value1, EcmaAssembly value2) => value1 == value2;
            protected override EcmaAssembly CreateValueFromKey(EcmaAssembly key) => throw new NotImplementedException();
            protected override int GetKeyHashCode(EcmaAssembly key) => key.GetHashCode();
            protected override int GetValueHashCode(EcmaAssembly value) => value.GetHashCode();
        }

        private readonly CompilerGeneratedState? _compilerGeneratedState;
        private readonly Logger _logger;
        private readonly AssemblyWarningsReportedHashtable _assemblyWarningsReportedHashtable;

        public UnconditionalSuppressMessageAttributeState(CompilerGeneratedState? compilerGeneratedState, Logger logger)
        {
            _compilerGeneratedState = compilerGeneratedState;
            _logger = logger;
            _assemblyWarningsReportedHashtable = new();
        }

        public bool IsSuppressed(int id, MessageOrigin warningOrigin)
        {
            // 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.

            TypeSystemEntity? provider = warningOrigin.MemberDefinition;
            if (provider == null)
                return false;

            if (IsSuppressed(id, provider))
                return true;

            if (_compilerGeneratedState != null)
            {
                while (_compilerGeneratedState.TryGetOwningMethodForCompilerGeneratedMember(provider, out MethodDesc? owningMethod))
                {
                    Debug.Assert(owningMethod != provider);
                    if (IsSuppressed(id, owningMethod))
                        return true;
                    provider = owningMethod;
                }
            }

            return false;
        }

        private bool IsSuppressed(int id, TypeSystemEntity? warningOrigin)
        {
            if (warningOrigin == null)
                return false;

            ModuleDesc? module = GetModuleFromProvider(warningOrigin);
            if (module is not EcmaAssembly ecmaAssembly)
                return false;

            // Only report the warnings if they were not reported already for this assembly
            List<(DiagnosticId, string?[])>? generatedWarnings = null;
            if (_assemblyWarningsReportedHashtable.TryAdd(ecmaAssembly))
                generatedWarnings = new();

            IEnumerable<Suppression>? moduleSuppressions = DecodeAssemblyAndModuleSuppressions(ecmaAssembly, generatedWarnings);

            if (generatedWarnings is not null)
            {
                foreach (var warning in generatedWarnings)
                {
                    _logger.LogWarning(ecmaAssembly, warning.Item1, warning.Item2);
                }
            }

            TypeSystemEntity? warningOriginMember = warningOrigin;
            while (warningOriginMember != null)
            {
                if (IsSuppressedOnElement(id, warningOriginMember, moduleSuppressions))
                    return true;

                if (warningOriginMember is MethodDesc method)
                {
                    if (method.GetPropertyForAccessor() is { } property)
                    {
                        Debug.Assert(property.OwningType == method.OwningType);
                        warningOriginMember = property;
                        continue;
                    }
                    else if (method.GetEventForAccessor() is { } @event)
                    {
                        Debug.Assert(@event.OwningType == method.OwningType);
                        warningOriginMember = @event;
                        continue;
                    }
                }

                warningOriginMember = warningOriginMember.GetOwningType();
            }

            // Check if there's an assembly or module level suppression.
            // Note that moduleSuppressions contains both assembly and module level suppressions all modified to target the module as the provider
            if (IsSuppressedOnElement(id, module, moduleSuppressions))
                return true;

            return false;
        }

        private static bool IsSuppressedOnElement(int id, TypeSystemEntity provider, IEnumerable<Suppression>? moduleSuppressions)
        {
            if (provider is not ModuleDesc)
            {
                foreach (var suppression in DecodeSuppressions(provider))
                {
                    if (suppression.SuppressMessageInfo.Id == id)
                        return true;
                }
            }

            if (moduleSuppressions is not null)
            {
                foreach (var suppression in moduleSuppressions)
                {
                    if (suppression.Provider == provider && suppression.SuppressMessageInfo.Id == id)
                        return true;
                }
            }

            return false;
        }

        private static bool TryDecodeSuppressMessageAttributeData(CustomAttributeValue<TypeDesc> 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.FixedArguments.Length < 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.FixedArguments[1].Value is string warningId) ||
                warningId.Length < 6 ||
                !warningId.StartsWith("IL", StringComparison.Ordinal) ||
                !int.TryParse(warningId.AsSpan(2, 4), out info.Id))
            {
                return false;
            }

            if (warningId.Length > 6 && warningId[6] != ':')
                return false;

            foreach (var p in attribute.NamedArguments)
            {
                switch (p.Name)
                {
                    case ScopeProperty when p.Value is string scope:
                        info.Scope = scope;
                        break;
                    case TargetProperty when p.Value is string target:
                        info.Target = target;
                        break;
                    case MessageIdProperty when p.Value is string messageId:
                        info.MessageId = messageId;
                        break;
                }
            }

            return true;
        }

        public static ModuleDesc? GetModuleFromProvider(TypeSystemEntity provider)
        {
            switch (provider)
            {
                case ModuleDesc module:
                    return module;
                case MetadataType type:
                    return type.Module;
                default:
                    return (provider.GetOwningType() as MetadataType)?.Module;
            }
        }

        private static IEnumerable<Suppression> DecodeSuppressions(TypeSystemEntity provider)
        {
            Debug.Assert(provider is not ModuleDesc);

            foreach (CustomAttributeValue<TypeDesc> ca in GetDecodedCustomAttributes(provider, UnconditionalSuppressMessageAttributeNamespace, UnconditionalSuppressMessageAttributeName))
            {
                if (!TryDecodeSuppressMessageAttributeData(ca, out var info))
                    continue;

                yield return new Suppression(info, originAttribute: ca, provider);
            }
        }

        private static List<Suppression>? DecodeAssemblyAndModuleSuppressions(EcmaAssembly ecmaAssembly, List<(DiagnosticId, string?[])>? warnings)
        {
            List<Suppression>? suppressions = null;
            DecodeGlobalSuppressions(
                ecmaAssembly,
                ecmaAssembly.GetDecodedCustomAttributes(UnconditionalSuppressMessageAttributeNamespace, UnconditionalSuppressMessageAttributeName),
                ref suppressions,
                warnings);

            DecodeGlobalSuppressions(
                ecmaAssembly,
                ecmaAssembly.GetDecodedCustomAttributesForModule(UnconditionalSuppressMessageAttributeNamespace, UnconditionalSuppressMessageAttributeName),
                ref suppressions,
                warnings);

            return suppressions;
        }

        private static void DecodeGlobalSuppressions(
            EcmaAssembly module,
            IEnumerable<CustomAttributeValue<TypeDesc>> attributes,
            ref List<Suppression>? suppressions,
            List<(DiagnosticId, string?[])>? warnings)
        {
            foreach (CustomAttributeValue<TypeDesc> instance in attributes)
            {
                if (!TryDecodeSuppressMessageAttributeData(instance, out SuppressMessageInfo info))
                    continue;

                var scope = info.Scope?.ToLowerInvariant();
                if (info.Target == null && (scope == "module" || scope == null))
                {
                    suppressions ??= new();
                    suppressions.Add(new Suppression(info, originAttribute: instance, module));
                    continue;
                }

                switch (scope)
                {
                    case "module":
                        suppressions ??= new();
                        suppressions.Add(new Suppression(info, originAttribute: instance, module));
                        break;

                    case "type":
                    case "member":
                        if (info.Target == null)
                            break;

                        foreach (var result in DocumentationSignatureParser.GetMembersForDocumentationSignature(info.Target, module))
                        {
                            suppressions ??= new();
                            suppressions.Add(new Suppression(info, originAttribute: instance, result));
                        }

                        break;
                    default:
                        warnings?.Add((DiagnosticId.InvalidScopeInUnconditionalSuppressMessage, new string?[] { info.Scope ?? "", module.GetName().Name, info.Target ?? "" }));
                        break;
                }
            }
        }

        private static IEnumerable<CustomAttributeValue<TypeDesc>> GetDecodedCustomAttributes(TypeSystemEntity entity, string attributeNamespace, string attributeName)
        {
            switch (entity)
            {
                case MethodDesc method:
                    if (method.GetTypicalMethodDefinition() is not EcmaMethod ecmaMethod)
                        return Enumerable.Empty<CustomAttributeValue<TypeDesc>>();
                    return ecmaMethod.GetDecodedCustomAttributes(attributeNamespace, attributeName);
                case MetadataType type:
                    if (type.GetTypeDefinition() is not EcmaType ecmaType)
                        return Enumerable.Empty<CustomAttributeValue<TypeDesc>>();
                    return ecmaType.GetDecodedCustomAttributes(attributeNamespace, attributeName);
                case FieldDesc field:
                    if (field.GetTypicalFieldDefinition() is not EcmaField ecmaField)
                        return Enumerable.Empty<CustomAttributeValue<TypeDesc>>();
                    return ecmaField.GetDecodedCustomAttributes(attributeNamespace, attributeName);
                case PropertyPseudoDesc property:
                    return property.GetDecodedCustomAttributes(attributeNamespace, attributeName);
                case EventPseudoDesc @event:
                    return @event.GetDecodedCustomAttributes(attributeNamespace, attributeName);
                default:
                    Debug.Fail("Trying to operate with unsupported TypeSystemEntity " + entity.GetType().ToString());
                    return Enumerable.Empty<CustomAttributeValue<TypeDesc>>();
            }
        }
    }
}