diff --git a/src/Shared/RoslynUtils/ParsabilityHelper.cs b/src/Shared/RoslynUtils/ParsabilityHelper.cs index 496510f5bbdc..a4e06e44388d 100644 --- a/src/Shared/RoslynUtils/ParsabilityHelper.cs +++ b/src/Shared/RoslynUtils/ParsabilityHelper.cs @@ -17,6 +17,9 @@ namespace Microsoft.AspNetCore.Analyzers.Infrastructure; internal static class ParsabilityHelper { + private static readonly BoundedCacheWithFactory BindabilityCache = new(); + private static readonly BoundedCacheWithFactory ParsabilityCache = new(); + private static bool IsTypeAlwaysParsable(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out ParsabilityMethod? parsabilityMethod) { // Any enum is valid. @@ -51,36 +54,41 @@ internal static Parsability GetParsability(ITypeSymbol typeSymbol, WellKnownType internal static Parsability GetParsability(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(false)] out ParsabilityMethod? parsabilityMethod) { - if (IsTypeAlwaysParsable(typeSymbol, wellKnownTypes, out parsabilityMethod)) - { - return Parsability.Parsable; - } + var parsability = Parsability.NotParsable; + parsabilityMethod = null; - // MyType : IParsable() - if (IsParsableViaIParsable(typeSymbol, wellKnownTypes)) + (parsability, parsabilityMethod) = ParsabilityCache.GetOrCreateValue(typeSymbol, (typeSymbol) => { - parsabilityMethod = ParsabilityMethod.IParsable; - return Parsability.Parsable; - } + if (IsTypeAlwaysParsable(typeSymbol, wellKnownTypes, out var parsabilityMethod)) + { + return (Parsability.Parsable, parsabilityMethod); + } - // Check if the parameter type has a public static TryParse method. - var tryParseMethods = typeSymbol.GetThisAndBaseTypes() - .SelectMany(t => t.GetMembers("TryParse")) - .OfType(); + // MyType : IParsable() + if (IsParsableViaIParsable(typeSymbol, wellKnownTypes)) + { + return (Parsability.Parsable, ParsabilityMethod.IParsable); + } - if (tryParseMethods.Any(m => IsTryParseWithFormat(m, wellKnownTypes))) - { - parsabilityMethod = ParsabilityMethod.TryParseWithFormatProvider; - return Parsability.Parsable; - } + // Check if the parameter type has a public static TryParse method. + var tryParseMethods = typeSymbol.GetThisAndBaseTypes() + .SelectMany(t => t.GetMembers("TryParse")) + .OfType(); - if (tryParseMethods.Any(IsTryParse)) - { - parsabilityMethod = ParsabilityMethod.TryParse; - return Parsability.Parsable; - } + if (tryParseMethods.Any(m => IsTryParseWithFormat(m, wellKnownTypes))) + { + return (Parsability.Parsable, ParsabilityMethod.TryParseWithFormatProvider); + } - return Parsability.NotParsable; + if (tryParseMethods.Any(IsTryParse)) + { + return (Parsability.Parsable, ParsabilityMethod.TryParse); + } + + return (Parsability.NotParsable, null); + }); + + return parsability; } private static bool IsTryParse(IMethodSymbol methodSymbol) @@ -155,35 +163,45 @@ internal static Bindability GetBindability(ITypeSymbol typeSymbol, WellKnownType { bindabilityMethod = null; bindMethodSymbol = null; + IMethodSymbol? bindAsyncMethod = null; - if (IsBindableViaIBindableFromHttpContext(typeSymbol, wellKnownTypes)) - { - bindabilityMethod = BindabilityMethod.IBindableFromHttpContext; - return Bindability.Bindable; - } - - // TODO: Search interfaces too. See MyBindAsyncFromInterfaceRecord test as an example. - // It's easy to find, but we need to flow the interface back to the emitter to call it. - // With parent types, we can continue to pretend we're calling a method directly on the child. - var bindAsyncMethods = typeSymbol.GetThisAndBaseTypes() - .Concat(typeSymbol.AllInterfaces) - .SelectMany(t => t.GetMembers("BindAsync")) - .OfType(); - - foreach (var methodSymbol in bindAsyncMethods) + (bindabilityMethod, bindMethodSymbol) = BindabilityCache.GetOrCreateValue(typeSymbol, (typeSymbol) => { - if (IsBindAsyncWithParameter(methodSymbol, typeSymbol, wellKnownTypes)) + BindabilityMethod? bindabilityMethod = null; + IMethodSymbol? bindMethodSymbol = null; + if (IsBindableViaIBindableFromHttpContext(typeSymbol, wellKnownTypes)) { - bindabilityMethod = BindabilityMethod.BindAsyncWithParameter; - bindMethodSymbol = methodSymbol; - break; + return (BindabilityMethod.IBindableFromHttpContext, null); } - if (IsBindAsync(methodSymbol, typeSymbol, wellKnownTypes)) + + var searchCandidates = typeSymbol.GetThisAndBaseTypes() + .Concat(typeSymbol.AllInterfaces); + + foreach (var candidate in searchCandidates) { - bindabilityMethod = BindabilityMethod.BindAsync; - bindMethodSymbol = methodSymbol; + var baseTypeBindAsyncMethods = candidate.GetMembers("BindAsync"); + foreach (var methodSymbolCandidate in baseTypeBindAsyncMethods) + { + if (methodSymbolCandidate is IMethodSymbol methodSymbol) + { + bindAsyncMethod ??= methodSymbol; + if (IsBindAsyncWithParameter(methodSymbol, typeSymbol, wellKnownTypes)) + { + bindabilityMethod = BindabilityMethod.BindAsyncWithParameter; + bindMethodSymbol = methodSymbol; + break; + } + if (IsBindAsync(methodSymbol, typeSymbol, wellKnownTypes)) + { + bindabilityMethod = BindabilityMethod.BindAsync; + bindMethodSymbol = methodSymbol; + } + } + } } - } + + return (bindabilityMethod, bindAsyncMethod); + }); if (bindabilityMethod is not null) { @@ -191,10 +209,8 @@ internal static Bindability GetBindability(ITypeSymbol typeSymbol, WellKnownType } // See if we can give better guidance on why the BindAsync method is no good. - if (bindAsyncMethods.Count() == 1) + if (bindAsyncMethod is not null) { - var bindAsyncMethod = bindAsyncMethods.Single(); - if (bindAsyncMethod.ReturnType is INamedTypeSymbol returnType && !IsReturningValueTaskOfTOrNullableT(returnType, typeSymbol, wellKnownTypes)) { return Bindability.InvalidReturnType;