File: Analyzers\JSImportExportDiagnosticsAnalyzer.cs
Web Access
Project: src\src\libraries\System.Runtime.InteropServices.JavaScript\gen\JSImportGenerator\JSImportGenerator.csproj (Microsoft.Interop.JavaScript.JSImportGenerator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
 
namespace Microsoft.Interop.JavaScript
{
    /// <summary>
    /// Abstract base analyzer for JSImport and JSExport diagnostics.
    /// Derived classes specialize for each attribute type.
    /// </summary>
    public abstract class JSInteropDiagnosticsAnalyzer : DiagnosticAnalyzer
    {
        // Diagnostic descriptors shared by both JSImport and JSExport analyzers.
        private static readonly ImmutableArray<DiagnosticDescriptor> s_sharedDescriptors = ImmutableArray.Create(
            GeneratorDiagnostics.ParameterTypeNotSupported,
            GeneratorDiagnostics.ReturnTypeNotSupported,
            GeneratorDiagnostics.ParameterTypeNotSupportedWithDetails,
            GeneratorDiagnostics.ReturnTypeNotSupportedWithDetails,
            GeneratorDiagnostics.ParameterConfigurationNotSupported,
            GeneratorDiagnostics.ReturnConfigurationNotSupported,
            GeneratorDiagnostics.ConfigurationNotSupported,
            GeneratorDiagnostics.ConfigurationValueNotSupported,
            GeneratorDiagnostics.MarshallingAttributeConfigurationNotSupported);
 
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
            s_sharedDescriptors
                .Add(InvalidSignatureDescriptor)
                .Add(ContainingTypeMissingModifiersDescriptor)
                .Add(RequiresAllowUnsafeBlocksDescriptor);
 
        /// <summary>The metadata name of the attribute this analyzer handles.</summary>
        protected abstract string AttributeMetadataName { get; }
 
        /// <summary>Descriptor for an invalid method signature for this attribute type.</summary>
        protected abstract DiagnosticDescriptor InvalidSignatureDescriptor { get; }
 
        /// <summary>Descriptor when the containing type is missing partial modifiers.</summary>
        protected abstract DiagnosticDescriptor ContainingTypeMissingModifiersDescriptor { get; }
 
        /// <summary>Descriptor reported when AllowUnsafeBlocks is not enabled.</summary>
        protected abstract DiagnosticDescriptor RequiresAllowUnsafeBlocksDescriptor { get; }
 
        /// <summary>
        /// When <see langword="true"/>, the method must have a body and must not be partial.
        /// When <see langword="false"/>, the method must not have a body and must be partial.
        /// </summary>
        protected abstract bool RequiresImplementation { get; }
 
        /// <summary>
        /// Calculate marshalling-related diagnostics for a validated method.
        /// </summary>
        protected abstract ImmutableArray<DiagnosticInfo> CalculateDiagnostics(
            MethodDeclarationSyntax methodSyntax,
            IMethodSymbol method,
            AttributeData attr,
            StubEnvironment environment,
            System.Threading.CancellationToken ct);
 
        public override void Initialize(AnalysisContext context)
        {
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
            context.EnableConcurrentExecution();
            context.RegisterCompilationStartAction(context =>
            {
                INamedTypeSymbol? attrType = context.Compilation.GetTypeByMetadataName(AttributeMetadataName);
                if (attrType is null)
                    return;
 
                StubEnvironment env = new StubEnvironment(
                    context.Compilation,
                    context.Compilation.GetEnvironmentFlags());
 
                bool unsafeEnabled = context.Compilation.Options is CSharpCompilationOptions { AllowUnsafe: true };
 
                int foundMethod = 0;
 
                context.RegisterSymbolAction(symbolContext =>
                {
                    IMethodSymbol method = (IMethodSymbol)symbolContext.Symbol;
                    foreach (AttributeData attr in method.GetAttributes())
                    {
                        if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attrType))
                        {
                            if (AnalyzeMethod(symbolContext, method, attr, env))
                            {
                                Interlocked.Exchange(ref foundMethod, 1);
                            }
                            break;
                        }
                    }
                }, SymbolKind.Method);
 
                if (!unsafeEnabled)
                {
                    context.RegisterCompilationEndAction(endContext =>
                    {
                        if (Volatile.Read(ref foundMethod) != 0)
                        {
                            endContext.ReportDiagnostic(DiagnosticInfo.Create(RequiresAllowUnsafeBlocksDescriptor, null).ToDiagnostic());
                        }
                    });
                }
            });
        }
 
        private bool AnalyzeMethod(SymbolAnalysisContext context, IMethodSymbol method, AttributeData attr, StubEnvironment env)
        {
            foreach (SyntaxReference syntaxRef in method.DeclaringSyntaxReferences)
            {
                if (syntaxRef.GetSyntax(context.CancellationToken) is MethodDeclarationSyntax methodSyntax)
                {
                    DiagnosticInfo? invalidMethodDiagnostic = GetDiagnosticIfInvalidMethodForGeneration(
                        methodSyntax, method,
                        InvalidSignatureDescriptor,
                        ContainingTypeMissingModifiersDescriptor,
                        RequiresImplementation);
                    if (invalidMethodDiagnostic is not null)
                    {
                        context.ReportDiagnostic(invalidMethodDiagnostic.ToDiagnostic());
                        return true;
                    }
 
                    foreach (DiagnosticInfo diagnostic in CalculateDiagnostics(methodSyntax, method, attr, env, context.CancellationToken))
                    {
                        context.ReportDiagnostic(diagnostic.ToDiagnostic());
                    }
                    break;
                }
            }
            return true;
        }
 
        /// <summary>
        /// Checks if a JSImport or JSExport method is invalid for generation and returns a diagnostic if so.
        /// </summary>
        /// <param name="invalidSignatureDescriptor">Descriptor for an invalid method signature.</param>
        /// <param name="containingTypeMissingModifiersDescriptor">Descriptor for a containing type missing modifiers.</param>
        /// <param name="requiresImplementation">
        /// When <see langword="true"/>, the method must have a body and must not be partial.
        /// When <see langword="false"/>, the method must not have a body and must be partial.
        /// </param>
        /// <returns>A diagnostic if the method is invalid, null otherwise.</returns>
        internal static DiagnosticInfo? GetDiagnosticIfInvalidMethodForGeneration(
            MethodDeclarationSyntax methodSyntax,
            IMethodSymbol method,
            DiagnosticDescriptor invalidSignatureDescriptor,
            DiagnosticDescriptor containingTypeMissingModifiersDescriptor,
            bool requiresImplementation)
        {
            bool hasImplementation = methodSyntax.Body is not null || methodSyntax.ExpressionBody is not null;
            bool isPartial = methodSyntax.Modifiers.Any(SyntaxKind.PartialKeyword);
 
            // requiresImplementation=false: must have no body, must be partial.
            // requiresImplementation=true: must have a body, must not be partial.
            if (methodSyntax.TypeParameterList is not null
                || hasImplementation != requiresImplementation
                || !methodSyntax.Modifiers.Any(SyntaxKind.StaticKeyword)
                || isPartial == requiresImplementation)
            {
                return DiagnosticInfo.Create(invalidSignatureDescriptor, methodSyntax.Identifier.GetLocation(), method.Name);
            }
 
            // Verify that the types the method is declared in are marked partial.
            if (methodSyntax.Parent is TypeDeclarationSyntax typeDecl && !typeDecl.IsInPartialContext(out var nonPartialIdentifier))
            {
                return DiagnosticInfo.Create(containingTypeMissingModifiersDescriptor, methodSyntax.Identifier.GetLocation(), method.Name, nonPartialIdentifier);
            }
 
            // Verify the method does not have a ref return
            if (method.ReturnsByRef || method.ReturnsByRefReadonly)
            {
                return DiagnosticInfo.Create(GeneratorDiagnostics.ReturnConfigurationNotSupported, methodSyntax.Identifier.GetLocation(), "ref return", method.ToDisplayString());
            }
 
            return null;
        }
    }
}