|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Collections.Generic; |
| 6 | +using System.Collections.Immutable; |
| 7 | +using Microsoft.CodeAnalysis; |
| 8 | +using Microsoft.CodeAnalysis.Diagnostics; |
| 9 | +using Microsoft.CodeAnalysis.Operations; |
| 10 | + |
| 11 | +namespace Microsoft.AspNetCore.Analyzers.Http; |
| 12 | + |
| 13 | +[DiagnosticAnalyzer(LanguageNames.CSharp)] |
| 14 | +public partial class HeaderDictionaryIndexerAnalyzer : DiagnosticAnalyzer |
| 15 | +{ |
| 16 | + public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.UseHeaderDictionaryPropertiesInsteadOfIndexer); |
| 17 | + |
| 18 | + public override void Initialize(AnalysisContext context) |
| 19 | + { |
| 20 | + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); |
| 21 | + context.EnableConcurrentExecution(); |
| 22 | + context.RegisterOperationAction(context => |
| 23 | + { |
| 24 | + var propertyReference = (IPropertyReferenceOperation)context.Operation; |
| 25 | + var property = propertyReference.Property; |
| 26 | + |
| 27 | + // Check if property is the indexer on IHeaderDictionary, e.g. headers["content-type"] |
| 28 | + if (property.IsIndexer && |
| 29 | + property.Parameters.Length == 1 && |
| 30 | + property.Parameters[0].Type.SpecialType == SpecialType.System_String && |
| 31 | + IsIHeadersDictionaryType(property.ContainingType)) |
| 32 | + { |
| 33 | + // Get the indexer string argument. |
| 34 | + if (propertyReference.Arguments.Length == 1 && |
| 35 | + propertyReference.Arguments[0].Value is ILiteralOperation literalOperation && |
| 36 | + literalOperation.ConstantValue.Value is string indexerValue) |
| 37 | + { |
| 38 | + // Check that the header has a matching property on IHeaderDictionary. |
| 39 | + if (PropertyMapping.TryGetValue(indexerValue, out var propertyName)) |
| 40 | + { |
| 41 | + AddDiagnosticWarning(context, propertyReference.Syntax.GetLocation(), indexerValue, propertyName); |
| 42 | + } |
| 43 | + } |
| 44 | + } |
| 45 | + }, OperationKind.PropertyReference); |
| 46 | + } |
| 47 | + |
| 48 | + private static bool IsIHeadersDictionaryType(INamedTypeSymbol type) |
| 49 | + { |
| 50 | + // Only IHeaderDictionary is valid. Types like HeaderDictionary, which implement IHeaderDictionary, |
| 51 | + // can't access header properties unless cast as IHeaderDictionary. |
| 52 | + return type is |
| 53 | + { |
| 54 | + Name: "IHeaderDictionary", |
| 55 | + ContainingNamespace: |
| 56 | + { |
| 57 | + Name: "Http", |
| 58 | + ContainingNamespace: |
| 59 | + { |
| 60 | + Name: "AspNetCore", |
| 61 | + ContainingNamespace: |
| 62 | + { |
| 63 | + Name: "Microsoft", |
| 64 | + ContainingNamespace: |
| 65 | + { |
| 66 | + IsGlobalNamespace: true |
| 67 | + } |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + }; |
| 72 | + } |
| 73 | + |
| 74 | + // Internal for unit tests |
| 75 | + // Note that this dictionary should be kept in sync with properties in IHeaderDictionary.Keyed.cs |
| 76 | + // Key = property name, Value = header name |
| 77 | + internal static readonly Dictionary<string, string> PropertyMapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) |
| 78 | + { |
| 79 | + ["Accept"] = "Accept", |
| 80 | + ["Accept-Charset"] = "AcceptCharset", |
| 81 | + ["Accept-Encoding"] = "AcceptEncoding", |
| 82 | + ["Accept-Language"] = "AcceptLanguage", |
| 83 | + ["Accept-Ranges"] = "AcceptRanges", |
| 84 | + ["Access-Control-Allow-Credentials"] = "AccessControlAllowCredentials", |
| 85 | + ["Access-Control-Allow-Headers"] = "AccessControlAllowHeaders", |
| 86 | + ["Access-Control-Allow-Methods"] = "AccessControlAllowMethods", |
| 87 | + ["Access-Control-Allow-Origin"] = "AccessControlAllowOrigin", |
| 88 | + ["Access-Control-Expose-Headers"] = "AccessControlExposeHeaders", |
| 89 | + ["Access-Control-Max-Age"] = "AccessControlMaxAge", |
| 90 | + ["Access-Control-Request-Headers"] = "AccessControlRequestHeaders", |
| 91 | + ["Access-Control-Request-Method"] = "AccessControlRequestMethod", |
| 92 | + ["Age"] = "Age", |
| 93 | + ["Allow"] = "Allow", |
| 94 | + ["Alt-Svc"] = "AltSvc", |
| 95 | + ["Authorization"] = "Authorization", |
| 96 | + ["baggage"] = "Baggage", |
| 97 | + ["Cache-Control"] = "CacheControl", |
| 98 | + ["Connection"] = "Connection", |
| 99 | + ["Content-Disposition"] = "ContentDisposition", |
| 100 | + ["Content-Encoding"] = "ContentEncoding", |
| 101 | + ["Content-Language"] = "ContentLanguage", |
| 102 | + ["Content-Location"] = "ContentLocation", |
| 103 | + ["Content-MD5"] = "ContentMD5", |
| 104 | + ["Content-Range"] = "ContentRange", |
| 105 | + ["Content-Security-Policy"] = "ContentSecurityPolicy", |
| 106 | + ["Content-Security-Policy-Report-Only"] = "ContentSecurityPolicyReportOnly", |
| 107 | + ["Content-Type"] = "ContentType", |
| 108 | + ["Correlation-Context"] = "CorrelationContext", |
| 109 | + ["Cookie"] = "Cookie", |
| 110 | + ["Date"] = "Date", |
| 111 | + ["ETag"] = "ETag", |
| 112 | + ["Expires"] = "Expires", |
| 113 | + ["Expect"] = "Expect", |
| 114 | + ["From"] = "From", |
| 115 | + ["Grpc-Accept-Encoding"] = "GrpcAcceptEncoding", |
| 116 | + ["Grpc-Encoding"] = "GrpcEncoding", |
| 117 | + ["Grpc-Message"] = "GrpcMessage", |
| 118 | + ["Grpc-Status"] = "GrpcStatus", |
| 119 | + ["Grpc-Timeout"] = "GrpcTimeout", |
| 120 | + ["Host"] = "Host", |
| 121 | + ["Keep-Alive"] = "KeepAlive", |
| 122 | + ["If-Match"] = "IfMatch", |
| 123 | + ["If-Modified-Since"] = "IfModifiedSince", |
| 124 | + ["If-None-Match"] = "IfNoneMatch", |
| 125 | + ["If-Range"] = "IfRange", |
| 126 | + ["If-Unmodified-Since"] = "IfUnmodifiedSince", |
| 127 | + ["Last-Modified"] = "LastModified", |
| 128 | + ["Link"] = "Link", |
| 129 | + ["Location"] = "Location", |
| 130 | + ["Max-Forwards"] = "MaxForwards", |
| 131 | + ["Origin"] = "Origin", |
| 132 | + ["Pragma"] = "Pragma", |
| 133 | + ["Proxy-Authenticate"] = "ProxyAuthenticate", |
| 134 | + ["Proxy-Authorization"] = "ProxyAuthorization", |
| 135 | + ["Proxy-Connection"] = "ProxyConnection", |
| 136 | + ["Range"] = "Range", |
| 137 | + ["Referer"] = "Referer", |
| 138 | + ["Retry-After"] = "RetryAfter", |
| 139 | + ["Request-Id"] = "RequestId", |
| 140 | + ["Sec-WebSocket-Accept"] = "SecWebSocketAccept", |
| 141 | + ["Sec-WebSocket-Key"] = "SecWebSocketKey", |
| 142 | + ["Sec-WebSocket-Protocol"] = "SecWebSocketProtocol", |
| 143 | + ["Sec-WebSocket-Version"] = "SecWebSocketVersion", |
| 144 | + ["Sec-WebSocket-Extensions"] = "SecWebSocketExtensions", |
| 145 | + ["Server"] = "Server", |
| 146 | + ["Set-Cookie"] = "SetCookie", |
| 147 | + ["Strict-Transport-Security"] = "StrictTransportSecurity", |
| 148 | + ["TE"] = "TE", |
| 149 | + ["Trailer"] = "Trailer", |
| 150 | + ["Transfer-Encoding"] = "TransferEncoding", |
| 151 | + ["Translate"] = "Translate", |
| 152 | + ["traceparent"] = "TraceParent", |
| 153 | + ["tracestate"] = "TraceState", |
| 154 | + ["Upgrade"] = "Upgrade", |
| 155 | + ["Upgrade-Insecure-Requests"] = "UpgradeInsecureRequests", |
| 156 | + ["User-Agent"] = "UserAgent", |
| 157 | + ["Vary"] = "Vary", |
| 158 | + ["Via"] = "Via", |
| 159 | + ["Warning"] = "Warning", |
| 160 | + ["WWW-Authenticate"] = "WWWAuthenticate", |
| 161 | + ["X-Content-Type-Options"] = "XContentTypeOptions", |
| 162 | + ["X-Frame-Options"] = "XFrameOptions", |
| 163 | + ["X-Powered-By"] = "XPoweredBy", |
| 164 | + ["X-Requested-With"] = "XRequestedWith", |
| 165 | + ["X-UA-Compatible"] = "XUACompatible", |
| 166 | + ["X-XSS-Protection"] = "XXSSProtection", |
| 167 | + }; |
| 168 | + |
| 169 | + private static void AddDiagnosticWarning(OperationAnalysisContext context, Location location, string headerName, string propertyName) |
| 170 | + { |
| 171 | + var propertiesBuilder = ImmutableDictionary.CreateBuilder<string, string>(); |
| 172 | + propertiesBuilder.Add("HeaderName", headerName); |
| 173 | + propertiesBuilder.Add("ResolvedPropertyName", propertyName); |
| 174 | + |
| 175 | + context.ReportDiagnostic(Diagnostic.Create( |
| 176 | + DiagnosticDescriptors.UseHeaderDictionaryPropertiesInsteadOfIndexer, |
| 177 | + location, |
| 178 | + propertiesBuilder.ToImmutable(), |
| 179 | + headerName, |
| 180 | + propertyName)); |
| 181 | + } |
| 182 | +} |
0 commit comments