File: RequiresAnalyzerBase.cs
Web Access
Project: src\src\tools\illink\src\ILLink.RoslynAnalyzer\ILLink.RoslynAnalyzer.csproj (ILLink.RoslynAnalyzer)
// 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.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ILLink.RoslynAnalyzer.DataFlow;
using ILLink.Shared;
using ILLink.Shared.DataFlow;
using ILLink.Shared.TrimAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>;
 
namespace ILLink.RoslynAnalyzer
{
    public abstract class RequiresAnalyzerBase : DiagnosticAnalyzer
    {
        private protected abstract string RequiresAttributeName { get; }
 
        internal abstract string RequiresAttributeFullyQualifiedName { get; }
 
        private protected abstract DiagnosticTargets AnalyzerDiagnosticTargets { get; }
 
        private protected abstract DiagnosticDescriptor RequiresDiagnosticRule { get; }
 
        private protected abstract DiagnosticId RequiresDiagnosticId { get; }
 
        private protected abstract DiagnosticDescriptor RequiresAttributeMismatch { get; }
        private protected abstract DiagnosticDescriptor RequiresOnStaticCtor { get; }
        private protected abstract DiagnosticDescriptor RequiresOnEntryPoint { get; }
 
        private protected virtual ImmutableArray<(Action<SyntaxNodeAnalysisContext> Action, SyntaxKind[] SyntaxKind)> ExtraSyntaxNodeActions { get; } = ImmutableArray<(Action<SyntaxNodeAnalysisContext> Action, SyntaxKind[] SyntaxKind)>.Empty;
        private protected virtual ImmutableArray<(Action<SymbolAnalysisContext> Action, SymbolKind[] SymbolKind)> ExtraSymbolActions { get; } = ImmutableArray<(Action<SymbolAnalysisContext> Action, SymbolKind[] SymbolKind)>.Empty;
 
        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
 
            if (!System.Diagnostics.Debugger.IsAttached)
                context.EnableConcurrentExecution();
 
            context.RegisterCompilationStartAction(context =>
            {
                var compilation = context.Compilation;
                if (!IsAnalyzerEnabled(context.Options))
                    return;
 
                var incompatibleMembers = GetSpecialIncompatibleMembers(compilation);
                context.RegisterSymbolAction(symbolAnalysisContext =>
                {
                    var methodSymbol = (IMethodSymbol)symbolAnalysisContext.Symbol;
 
                    if (methodSymbol.HasAttribute(RequiresAttributeName))
                    {
                        if (methodSymbol.IsStaticConstructor())
                            ReportRequiresOnStaticCtorDiagnostic(symbolAnalysisContext, methodSymbol);
 
                        if (methodSymbol.IsEntryPoint(symbolAnalysisContext.Compilation) || methodSymbol.IsUnmanagedCallersOnlyEntryPoint())
                            ReportRequiresOnEntryPointDiagnostic(symbolAnalysisContext, methodSymbol);
                    }
 
                    CheckMatchingAttributesInOverrides(symbolAnalysisContext, methodSymbol);
                }, SymbolKind.Method);
 
                context.RegisterSymbolAction(symbolAnalysisContext =>
                {
                    var typeSymbol = (INamedTypeSymbol)symbolAnalysisContext.Symbol;
                    CheckMatchingAttributesInInterfaces(symbolAnalysisContext, typeSymbol);
                }, SymbolKind.NamedType);
 
                context.RegisterSyntaxNodeAction(syntaxNodeAnalysisContext =>
                {
                    var model = syntaxNodeAnalysisContext.SemanticModel;
                    if (syntaxNodeAnalysisContext.ContainingSymbol is not ISymbol containingSymbol || containingSymbol.IsInRequiresScope(RequiresAttributeName, out _))
                        return;
 
                    GenericNameSyntax genericNameSyntaxNode = (GenericNameSyntax)syntaxNodeAnalysisContext.Node;
                    var typeParams = ImmutableArray<ITypeParameterSymbol>.Empty;
                    var typeArgs = ImmutableArray<ITypeSymbol>.Empty;
                    switch (model.GetSymbolInfo(genericNameSyntaxNode).Symbol)
                    {
                        case INamedTypeSymbol typeSymbol:
                            typeParams = typeSymbol.TypeParameters;
                            typeArgs = typeSymbol.TypeArguments;
                            break;
 
                        case IMethodSymbol methodSymbol:
                            typeParams = methodSymbol.TypeParameters;
                            typeArgs = methodSymbol.TypeArguments;
                            break;
 
                        default:
                            return;
                    }
 
                    for (int i = 0; i < typeParams.Length; i++)
                    {
                        var typeParam = typeParams[i];
                        var typeArg = typeArgs[i];
                        if (!typeParam.HasConstructorConstraint ||
                            typeArg is not INamedTypeSymbol { InstanceConstructors: { } typeArgCtors })
                            continue;
 
                        foreach (var instanceCtor in typeArgCtors)
                        {
                            if (instanceCtor.Arity > 0)
                                continue;
 
                            if (instanceCtor.DoesMemberRequire(RequiresAttributeName, out var requiresAttribute) &&
                                VerifyAttributeArguments(requiresAttribute))
                            {
                                syntaxNodeAnalysisContext.ReportDiagnostic(Diagnostic.Create(RequiresDiagnosticRule,
                                    syntaxNodeAnalysisContext.Node.GetLocation(),
                                    instanceCtor.GetDisplayName(),
                                    (string)requiresAttribute.ConstructorArguments[0].Value!,
                                    GetUrlFromAttribute(requiresAttribute)));
                            }
                        }
                    }
                }, SyntaxKind.GenericName);
 
                foreach (var extraSyntaxNodeAction in ExtraSyntaxNodeActions)
                    context.RegisterSyntaxNodeAction(extraSyntaxNodeAction.Action, extraSyntaxNodeAction.SyntaxKind);
 
                foreach (var extraSymbolAction in ExtraSymbolActions)
                    context.RegisterSymbolAction(extraSymbolAction.Action, extraSymbolAction.SymbolKind);
 
                void CheckMatchingAttributesInOverrides(
                    SymbolAnalysisContext symbolAnalysisContext,
                    ISymbol member)
                {
                    if ((member.IsVirtual || member.IsOverride) && member.TryGetOverriddenMember(out var overriddenMember) && HasMismatchingAttributes(member, overriddenMember))
                        ReportMismatchInAttributesDiagnostic(symbolAnalysisContext, member, overriddenMember);
                }
 
                void CheckMatchingAttributesInInterfaces(
                    SymbolAnalysisContext symbolAnalysisContext,
                    INamedTypeSymbol type)
                {
                    foreach (var memberpair in type.GetMemberInterfaceImplementationPairs())
                    {
                        var implementationType = memberpair.ImplementationMember switch
                        {
                            IMethodSymbol method => method.ContainingType,
                            IPropertySymbol property => property.ContainingType,
                            IEventSymbol @event => @event.ContainingType,
                            _ => throw new NotSupportedException()
                        };
                        ISymbol origin = memberpair.ImplementationMember;
 
                        // If this type implements an interface method through a base class, the origin of the warning is this type,
                        // not the member on the base class.
                        if (!implementationType.IsInterface() && !SymbolEqualityComparer.Default.Equals(implementationType, type))
                            origin = type;
 
                        if (HasMismatchingAttributes(memberpair.InterfaceMember, memberpair.ImplementationMember))
                        {
                            ReportMismatchInAttributesDiagnostic(symbolAnalysisContext, memberpair.ImplementationMember, memberpair.InterfaceMember, isInterface: true, origin);
                        }
                    }
                }
            });
        }
 
        internal void CheckAndCreateRequiresDiagnostic(
            ISymbol member,
            ISymbol containingSymbol,
            ImmutableArray<ISymbol> incompatibleMembers,
            in DiagnosticContext diagnosticContext)
        {
            // Do not emit any diagnostic if caller is annotated with the attribute too.
            if (containingSymbol.IsInRequiresScope(RequiresAttributeName, out _))
                return;
 
            if (CreateSpecialIncompatibleMembersDiagnostic(incompatibleMembers, member, diagnosticContext))
                return;
 
            // Warn on the most derived base method taking into account covariant returns
            while (member is IMethodSymbol method && method.OverriddenMethod != null && SymbolEqualityComparer.Default.Equals(method.ReturnType, method.OverriddenMethod.ReturnType))
                member = method.OverriddenMethod;
 
            if (!member.DoesMemberRequire(RequiresAttributeName, out var requiresAttribute))
                return;
 
            if (!VerifyAttributeArguments(requiresAttribute))
                return;
 
            CreateRequiresDiagnostic(member, requiresAttribute, diagnosticContext);
        }
 
        [Flags]
        protected enum DiagnosticTargets
        {
            MethodOrConstructor = 0x0001,
            Property = 0x0002,
            Field = 0x0004,
            Event = 0x0008,
            Class = 0x0010,
            All = MethodOrConstructor | Property | Field | Event | Class
        }
 
        /// <summary>
        /// Finds the symbol of the caller to the current operation, helps to find out the symbol in cases where the operation passes
        /// through a lambda or a local function.
        /// </summary>
        /// <param name="operationContext">Analyzer operation context to retrieve the current operation.</param>
        /// <param name="targets">Scope of the attribute to search for callers.</param>
        /// <returns>The symbol of the caller to the operation</returns>
        protected static ISymbol FindContainingSymbol(OperationAnalysisContext operationContext, DiagnosticTargets targets)
        {
            var parent = operationContext.Operation.Parent;
            while (parent is not null)
            {
                switch (parent)
                {
                    case IAnonymousFunctionOperation lambda:
                        return lambda.Symbol;
 
                    case ILocalFunctionOperation local when targets.HasFlag(DiagnosticTargets.MethodOrConstructor):
                        return local.Symbol;
 
                    case IMethodBodyBaseOperation when targets.HasFlag(DiagnosticTargets.MethodOrConstructor):
                    case IPropertyReferenceOperation when targets.HasFlag(DiagnosticTargets.Property):
                    case IFieldReferenceOperation when targets.HasFlag(DiagnosticTargets.Field):
                    case IEventReferenceOperation when targets.HasFlag(DiagnosticTargets.Event):
                        return operationContext.ContainingSymbol;
 
                    default:
                        parent = parent.Parent;
                        break;
                }
            }
 
            return operationContext.ContainingSymbol;
        }
 
        /// <summary>
        /// Creates a Requires diagnostic message based on the attribute data and RequiresDiagnosticRule.
        /// </summary>
        /// <param name="operationContext">Analyzer operation context to be able to report the diagnostic.</param>
        /// <param name="member">Information about the member that generated the diagnostic.</param>
        /// <param name="requiresAttribute">Requires attribute data to print attribute arguments.</param>
        private void CreateRequiresDiagnostic(ISymbol member, AttributeData requiresAttribute, in DiagnosticContext diagnosticContext)
        {
            var message = GetMessageFromAttribute(requiresAttribute);
            var url = GetUrlFromAttribute(requiresAttribute);
            diagnosticContext.AddDiagnostic(RequiresDiagnosticId, member.GetDisplayName(), message, url);
        }
 
        private void ReportRequiresOnStaticCtorDiagnostic(SymbolAnalysisContext symbolAnalysisContext, IMethodSymbol ctor)
        {
            symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(
                RequiresOnStaticCtor,
                ctor.Locations[0],
                ctor.GetDisplayName()));
        }
 
        private void ReportRequiresOnEntryPointDiagnostic(SymbolAnalysisContext symbolAnalysisContext, IMethodSymbol entryPoint)
        {
            symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(
                RequiresOnEntryPoint,
                entryPoint.Locations[0],
                entryPoint.GetDisplayName()));
        }
 
        private void ReportMismatchInAttributesDiagnostic(SymbolAnalysisContext symbolAnalysisContext, ISymbol member, ISymbol baseMember, bool isInterface = false, ISymbol? origin = null)
        {
            origin ??= member;
            string message = MessageFormat.FormatRequiresAttributeMismatch(member.HasAttribute(RequiresAttributeName), isInterface, RequiresAttributeName, member.GetDisplayName(), baseMember.GetDisplayName());
            symbolAnalysisContext.ReportDiagnostic(Diagnostic.Create(
                RequiresAttributeMismatch,
                origin.Locations[0],
                message));
        }
 
        private bool HasMismatchingAttributes(ISymbol member1, ISymbol member2)
        {
            bool member1CreatesRequirement = member1.DoesMemberRequire(RequiresAttributeName, out _);
            bool member2CreatesRequirement = member2.DoesMemberRequire(RequiresAttributeName, out _);
            bool member1FulfillsRequirement = member1.IsInRequiresScope(RequiresAttributeName);
            bool member2FulfillsRequirement = member2.IsInRequiresScope(RequiresAttributeName);
            return (member1CreatesRequirement && !member2FulfillsRequirement) || (member2CreatesRequirement && !member1FulfillsRequirement);
        }
 
        protected abstract string GetMessageFromAttribute(AttributeData requiresAttribute);
 
        public static string GetUrlFromAttribute(AttributeData? requiresAttribute)
        {
            var url = requiresAttribute?.NamedArguments.FirstOrDefault(na => na.Key == "Url").Value.Value?.ToString();
            return MessageFormat.FormatRequiresAttributeUrlArg(url);
        }
 
        /// <summary>
        /// This method verifies that the arguments in an attribute have certain structure.
        /// </summary>
        /// <param name="attribute">Attribute data to compare.</param>
        /// <returns>True if the validation was successfull; otherwise, returns false.</returns>
        protected abstract bool VerifyAttributeArguments(AttributeData attribute);
 
        /// <summary>
        /// Compares the member against a list of incompatible members, if the member exist in the list then it generates a custom diagnostic declared inside the function.
        /// </summary>
        /// <param name="operationContext">Analyzer operation context.</param>
        /// <param name="specialIncompatibleMembers">List of incompatible members.</param>
        /// <param name="member">Member to compare.</param>
        /// <returns>True if the function generated a diagnostic; otherwise, returns false</returns>
        protected virtual bool CreateSpecialIncompatibleMembersDiagnostic(
            ImmutableArray<ISymbol> specialIncompatibleMembers,
            ISymbol member,
            in DiagnosticContext diagnosticContext)
        {
            return false;
        }
 
        /// <summary>
        /// Creates a list of special incompatible members that can be used later on by the analyzer to generate diagnostics
        /// </summary>
        /// <param name="compilation">Compilation to search for members</param>
        /// <returns>A list of special incomptaible members</returns>
        internal virtual ImmutableArray<ISymbol> GetSpecialIncompatibleMembers(Compilation compilation) => default;
 
        /// <summary>
        /// Verifies that the MSBuild requirements to run the analyzer are fulfilled
        /// </summary>
        /// <param name="options">Analyzer options</param>
        /// <returns>True if the requirements to run the analyzer are met; otherwise, returns false</returns>
        internal abstract bool IsAnalyzerEnabled(AnalyzerOptions options);
 
        // Check whether a given property serves as a check for the "feature" or "capability" associated with the attribute
        // understood by this analyzer. For now, this is only designed to support checks like
        // RuntimeFeatures.IsDynamicCodeSupported, where a true return value indicates that the feature is supported.
        // This doesn't support more general cases such as:
        // - false return value indicating that a feature is supported
        // - feature settings supplied by the project
        // - custom feature checks defined in library code
        private protected virtual bool IsRequiresCheck(IPropertySymbol propertySymbol, Compilation compilation) => false;
 
        internal static bool IsAnnotatedFeatureGuard(IPropertySymbol propertySymbol, string featureName)
        {
            // Only respect FeatureGuardAttribute on static boolean properties.
            if (!propertySymbol.IsStatic || propertySymbol.Type.SpecialType != SpecialType.System_Boolean || propertySymbol.SetMethod != null)
                return false;
 
            ValueSet<string> featureCheckAnnotations = propertySymbol.GetFeatureGuardAnnotations();
            return featureCheckAnnotations.Contains(featureName);
        }
 
        internal bool IsFeatureGuard(IPropertySymbol propertySymbol, Compilation compilation)
        {
            return IsAnnotatedFeatureGuard(propertySymbol, RequiresAttributeFullyQualifiedName)
                || IsRequiresCheck(propertySymbol, compilation);
        }
 
        internal void CheckAndCreateRequiresDiagnostic(
            IOperation operation,
            ISymbol member,
            ISymbol owningSymbol,
            DataFlowAnalyzerContext context,
            FeatureContext featureContext,
            in DiagnosticContext diagnosticContext)
        {
            // Warnings are not emitted if the featureContext says the feature is available.
            if (featureContext.IsEnabled(RequiresAttributeFullyQualifiedName))
                return;
 
            ISymbol containingSymbol = operation.FindContainingSymbol(owningSymbol);
 
            var incompatibleMembers = context.GetSpecialIncompatibleMembers(this);
            CheckAndCreateRequiresDiagnostic(
                member,
                containingSymbol,
                incompatibleMembers,
                diagnosticContext);
        }
 
        internal virtual bool IsIntrinsicallyHandled(
            IMethodSymbol calledMethod,
            MultiValue instance,
            ImmutableArray<MultiValue> arguments
            )
        {
            return false;
        }
    }
}