File: Http\HeaderDictionaryIndexerAnalyzer.cs
Web Access
Project: src\src\Framework\AspNetCoreAnalyzers\src\Analyzers\Microsoft.AspNetCore.App.Analyzers.csproj (Microsoft.AspNetCore.App.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
 
namespace Microsoft.AspNetCore.Analyzers.Http;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public partial class HeaderDictionaryIndexerAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseHeaderDictionaryPropertiesInsteadOfIndexer);
 
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterOperationAction(context =>
        {
            var propertyReference = (IPropertyReferenceOperation)context.Operation;
            var property = propertyReference.Property;
 
            // Check if property is the indexer on IHeaderDictionary, e.g. headers["content-type"]
            if (property.IsIndexer &&
                property.Parameters.Length == 1 &&
                property.Parameters[0].Type.SpecialType == SpecialType.System_String &&
                IsIHeadersDictionaryType(property.ContainingType))
            {
                // Get the indexer string argument.
                if (propertyReference.Arguments.Length == 1 &&
                    propertyReference.Arguments[0].Value is ILiteralOperation literalOperation &&
                    literalOperation.ConstantValue.Value is string indexerValue)
                {
                    // Check that the header has a matching property on IHeaderDictionary.
                    if (PropertyMapping.TryGetValue(indexerValue, out var propertyName))
                    {
                        AddDiagnosticWarning(context, propertyReference.Syntax.GetLocation(), indexerValue, propertyName);
                    }
                }
            }
        }, OperationKind.PropertyReference);
    }
 
    private static bool IsIHeadersDictionaryType(INamedTypeSymbol type)
    {
        // Only IHeaderDictionary is valid. Types like HeaderDictionary, which implement IHeaderDictionary,
        // can't access header properties unless cast as IHeaderDictionary.
        return type is
        {
            Name: "IHeaderDictionary",
            ContainingNamespace:
            {
                Name: "Http",
                ContainingNamespace:
                {
                    Name: "AspNetCore",
                    ContainingNamespace:
                    {
                        Name: "Microsoft",
                        ContainingNamespace:
                        {
                            IsGlobalNamespace: true
                        }
                    }
                }
            }
        };
    }
 
    // Internal for unit tests
    // Note that this dictionary should be kept in sync with properties in IHeaderDictionary.Keyed.cs
    // Key = property name, Value = header name
    internal static readonly Dictionary<string, string> PropertyMapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        ["Accept"] = "Accept",
        ["Accept-Charset"] = "AcceptCharset",
        ["Accept-Encoding"] = "AcceptEncoding",
        ["Accept-Language"] = "AcceptLanguage",
        ["Accept-Ranges"] = "AcceptRanges",
        ["Access-Control-Allow-Credentials"] = "AccessControlAllowCredentials",
        ["Access-Control-Allow-Headers"] = "AccessControlAllowHeaders",
        ["Access-Control-Allow-Methods"] = "AccessControlAllowMethods",
        ["Access-Control-Allow-Origin"] = "AccessControlAllowOrigin",
        ["Access-Control-Expose-Headers"] = "AccessControlExposeHeaders",
        ["Access-Control-Max-Age"] = "AccessControlMaxAge",
        ["Access-Control-Request-Headers"] = "AccessControlRequestHeaders",
        ["Access-Control-Request-Method"] = "AccessControlRequestMethod",
        ["Age"] = "Age",
        ["Allow"] = "Allow",
        ["Alt-Svc"] = "AltSvc",
        ["Authorization"] = "Authorization",
        ["baggage"] = "Baggage",
        ["Cache-Control"] = "CacheControl",
        ["Connection"] = "Connection",
        ["Content-Disposition"] = "ContentDisposition",
        ["Content-Encoding"] = "ContentEncoding",
        ["Content-Language"] = "ContentLanguage",
        ["Content-Location"] = "ContentLocation",
        ["Content-MD5"] = "ContentMD5",
        ["Content-Range"] = "ContentRange",
        ["Content-Security-Policy"] = "ContentSecurityPolicy",
        ["Content-Security-Policy-Report-Only"] = "ContentSecurityPolicyReportOnly",
        ["Content-Type"] = "ContentType",
        ["Correlation-Context"] = "CorrelationContext",
        ["Cookie"] = "Cookie",
        ["Date"] = "Date",
        ["ETag"] = "ETag",
        ["Expires"] = "Expires",
        ["Expect"] = "Expect",
        ["From"] = "From",
        ["Grpc-Accept-Encoding"] = "GrpcAcceptEncoding",
        ["Grpc-Encoding"] = "GrpcEncoding",
        ["Grpc-Message"] = "GrpcMessage",
        ["Grpc-Status"] = "GrpcStatus",
        ["Grpc-Timeout"] = "GrpcTimeout",
        ["Host"] = "Host",
        ["Keep-Alive"] = "KeepAlive",
        ["If-Match"] = "IfMatch",
        ["If-Modified-Since"] = "IfModifiedSince",
        ["If-None-Match"] = "IfNoneMatch",
        ["If-Range"] = "IfRange",
        ["If-Unmodified-Since"] = "IfUnmodifiedSince",
        ["Last-Modified"] = "LastModified",
        ["Link"] = "Link",
        ["Location"] = "Location",
        ["Max-Forwards"] = "MaxForwards",
        ["Origin"] = "Origin",
        ["Pragma"] = "Pragma",
        ["Proxy-Authenticate"] = "ProxyAuthenticate",
        ["Proxy-Authorization"] = "ProxyAuthorization",
        ["Proxy-Connection"] = "ProxyConnection",
        ["Range"] = "Range",
        ["Referer"] = "Referer",
        ["Retry-After"] = "RetryAfter",
        ["Request-Id"] = "RequestId",
        ["Sec-WebSocket-Accept"] = "SecWebSocketAccept",
        ["Sec-WebSocket-Key"] = "SecWebSocketKey",
        ["Sec-WebSocket-Protocol"] = "SecWebSocketProtocol",
        ["Sec-WebSocket-Version"] = "SecWebSocketVersion",
        ["Sec-WebSocket-Extensions"] = "SecWebSocketExtensions",
        ["Server"] = "Server",
        ["Set-Cookie"] = "SetCookie",
        ["Strict-Transport-Security"] = "StrictTransportSecurity",
        ["TE"] = "TE",
        ["Trailer"] = "Trailer",
        ["Transfer-Encoding"] = "TransferEncoding",
        ["Translate"] = "Translate",
        ["traceparent"] = "TraceParent",
        ["tracestate"] = "TraceState",
        ["Upgrade"] = "Upgrade",
        ["Upgrade-Insecure-Requests"] = "UpgradeInsecureRequests",
        ["User-Agent"] = "UserAgent",
        ["Vary"] = "Vary",
        ["Via"] = "Via",
        ["Warning"] = "Warning",
        ["WWW-Authenticate"] = "WWWAuthenticate",
        ["X-Content-Type-Options"] = "XContentTypeOptions",
        ["X-Frame-Options"] = "XFrameOptions",
        ["X-Powered-By"] = "XPoweredBy",
        ["X-Requested-With"] = "XRequestedWith",
        ["X-UA-Compatible"] = "XUACompatible",
        ["X-XSS-Protection"] = "XXSSProtection",
    };
 
    private static void AddDiagnosticWarning(OperationAnalysisContext context, Location location, string headerName, string propertyName)
    {
        var propertiesBuilder = ImmutableDictionary.CreateBuilder<string, string?>();
        propertiesBuilder.Add("HeaderName", headerName);
        propertiesBuilder.Add("ResolvedPropertyName", propertyName);
 
        context.ReportDiagnostic(Diagnostic.Create(
            DiagnosticDescriptors.UseHeaderDictionaryPropertiesInsteadOfIndexer,
            location,
            propertiesBuilder.ToImmutable(),
            headerName,
            propertyName));
    }
}