File: TagHelpersInCodeBlocksAnalyzer.cs
Web Access
Project: src\src\Mvc\Mvc.Analyzers\src\Microsoft.AspNetCore.Mvc.Analyzers.csproj (Microsoft.AspNetCore.Mvc.Analyzers)
// 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.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
 
namespace Microsoft.AspNetCore.Mvc.Analyzers;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class TagHelpersInCodeBlocksAnalyzer : DiagnosticAnalyzer
{
    public TagHelpersInCodeBlocksAnalyzer()
    {
        SupportedDiagnostics = ImmutableArray.Create(DiagnosticDescriptors.MVC1006_FunctionsContainingTagHelpersMustBeAsyncAndReturnTask);
    }
 
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
 
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        // Generated Razor code is considered auto generated. By default analyzers skip over auto-generated code unless we say otherwise.
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
        context.RegisterCompilationStartAction(context =>
        {
            if (!SymbolCache.TryCreate(context.Compilation, out var symbolCache))
            {
                // No-op if we can't find types we care about.
                return;
            }
 
            InitializeWorker(context, symbolCache);
        });
    }
 
    private static void InitializeWorker(CompilationStartAnalysisContext context, SymbolCache symbolCache)
    {
        context.RegisterOperationBlockStartAction(startBlockContext =>
        {
            var capturedDiagnosticLocations = new HashSet<Location>();
            startBlockContext.RegisterOperationAction(context =>
            {
                var awaitOperation = (IAwaitOperation)context.Operation;
 
                if (awaitOperation.Operation.Kind != OperationKind.Invocation)
                {
                    return;
                }
 
                var invocationOperation = (IInvocationOperation)awaitOperation.Operation;
 
                if (!IsTagHelperRunnerRunAsync(invocationOperation.TargetMethod, symbolCache))
                {
                    return;
                }
 
                var parent = context.Operation.Parent;
                while (parent != null && !IsParentMethod(parent))
                {
                    parent = parent.Parent;
                }
 
                if (parent == null)
                {
                    return;
                }
 
                var methodSymbol = (IMethodSymbol?)(parent switch
                {
                    ILocalFunctionOperation localFunctionOperation => localFunctionOperation.Symbol,
                    IAnonymousFunctionOperation anonymousFunctionOperation => anonymousFunctionOperation.Symbol,
                    IMethodBodyOperation methodBodyOperation => startBlockContext.OwningSymbol,
                    _ => null,
                });
 
                if (methodSymbol == null)
                {
                    // Unsupported operation type.
                    return;
                }
 
                if (!methodSymbol.IsAsync ||
                    !symbolCache.TaskType.IsAssignableFrom(methodSymbol.ReturnType))
                {
                    capturedDiagnosticLocations.Add(parent.Syntax.GetLocation());
                }
            }, OperationKind.Await);
 
            startBlockContext.RegisterOperationBlockEndAction(context =>
            {
                foreach (var location in capturedDiagnosticLocations)
                {
                    context.ReportDiagnostic(
                        Diagnostic.Create(DiagnosticDescriptors.MVC1006_FunctionsContainingTagHelpersMustBeAsyncAndReturnTask, location));
                }
            });
        });
    }
 
    private static bool IsTagHelperRunnerRunAsync(IMethodSymbol method, SymbolCache symbolCache)
    {
        if (!SymbolEqualityComparer.Default.Equals(method, symbolCache.TagHelperRunnerRunAsyncMethodSymbol))
        {
            return false;
        }
 
        return true;
    }
 
    private static bool IsParentMethod(IOperation operation)
    {
        if (operation.Kind == OperationKind.LocalFunction)
        {
            return true;
        }
 
        if (operation.Kind == OperationKind.MethodBody)
        {
            return true;
        }
 
        if (operation.Kind == OperationKind.AnonymousFunction)
        {
            return true;
        }
 
        return false;
    }
 
    private readonly struct SymbolCache
    {
        private SymbolCache(
            IMethodSymbol tagHelperRunnerRunAsyncMethodSymbol,
            INamedTypeSymbol taskType)
        {
            TagHelperRunnerRunAsyncMethodSymbol = tagHelperRunnerRunAsyncMethodSymbol;
            TaskType = taskType;
        }
 
        public IMethodSymbol TagHelperRunnerRunAsyncMethodSymbol { get; }
 
        public INamedTypeSymbol TaskType { get; }
 
        public static bool TryCreate(Compilation compilation, out SymbolCache symbolCache)
        {
            symbolCache = default;
 
            if (!TryGetType(SymbolNames.TagHelperRunnerTypeName, out var tagHelperRunnerType))
            {
                return false;
            }
 
            if (!TryGetType(SymbolNames.TaskTypeName, out var taskType))
            {
                return false;
            }
 
            var members = tagHelperRunnerType.GetMembers(SymbolNames.RunAsyncMethodName);
            if (members.Length == 0)
            {
                return false;
            }
 
            var tagHelperRunnerRunAsyncMethod = (IMethodSymbol)members[0];
 
            symbolCache = new SymbolCache(tagHelperRunnerRunAsyncMethod, taskType);
            return true;
 
            bool TryGetType(string typeName, out INamedTypeSymbol typeSymbol)
            {
                typeSymbol = compilation.GetTypeByMetadataName(typeName);
                return typeSymbol != null && typeSymbol.TypeKind != TypeKind.Error;
            }
        }
    }
}