File: ApiConventionAnalyzer.cs
Web Access
Project: src\src\Mvc\Mvc.Api.Analyzers\src\Microsoft.AspNetCore.Mvc.Api.Analyzers.csproj (Microsoft.AspNetCore.Mvc.Api.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.Api.Analyzers;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ApiConventionAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
        ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode,
        ApiDiagnosticDescriptors.API1001_ActionReturnsUndocumentedSuccessResult,
        ApiDiagnosticDescriptors.API1002_ActionDoesNotReturnDocumentedStatusCode);
 
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
 
        context.RegisterCompilationStartAction(context =>
        {
            if (!ApiControllerSymbolCache.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, ApiControllerSymbolCache symbolCache)
    {
        context.RegisterOperationAction(context =>
        {
            var method = (IMethodSymbol)context.ContainingSymbol;
            if (!ApiControllerFacts.IsApiControllerAction(symbolCache, method))
            {
                return;
            }
 
            var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
            var hasUnreadableStatusCodes = !ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, (IMethodBodyOperation)context.Operation, out var actualResponseMetadata);
 
            var hasUndocumentedStatusCodes = false;
            foreach (var actualMetadata in actualResponseMetadata)
            {
                var location = actualMetadata.ReturnOperation.ReturnedValue.Syntax.GetLocation();
 
                if (!DeclaredApiResponseMetadata.Contains(declaredResponseMetadata, actualMetadata))
                {
                    hasUndocumentedStatusCodes = true;
                    if (actualMetadata.IsDefaultResponse)
                    {
                        context.ReportDiagnostic(Diagnostic.Create(
                            ApiDiagnosticDescriptors.API1001_ActionReturnsUndocumentedSuccessResult,
                            location));
                    }
                    else
                    {
                        context.ReportDiagnostic(Diagnostic.Create(
                            ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode,
                            location,
                               actualMetadata.StatusCode));
                    }
                }
            }
 
            if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes)
            {
                // If we produced analyzer warnings about undocumented status codes, don't attempt to determine
                // if there are documented status codes that are missing from the method body.
                return;
            }
 
            for (var i = 0; i < declaredResponseMetadata.Count; i++)
            {
                var declaredMetadata = declaredResponseMetadata[i];
                if (!Contains(actualResponseMetadata, declaredMetadata))
                {
                    context.ReportDiagnostic(Diagnostic.Create(
                        ApiDiagnosticDescriptors.API1002_ActionDoesNotReturnDocumentedStatusCode,
                        method.Locations[0],
                        declaredMetadata.StatusCode));
                }
            }
        }, OperationKind.MethodBody);
    }
 
    internal static bool Contains(IList<ActualApiResponseMetadata> actualResponseMetadata, DeclaredApiResponseMetadata declaredMetadata)
    {
        for (var i = 0; i < actualResponseMetadata.Count; i++)
        {
            if (declaredMetadata.Matches(actualResponseMetadata[i]))
            {
                return true;
            }
        }
 
        return false;
    }
}