Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 66 additions & 50 deletions src/Shared/RoslynUtils/ParsabilityHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ namespace Microsoft.AspNetCore.Analyzers.Infrastructure;

internal static class ParsabilityHelper
{
private static readonly BoundedCacheWithFactory<ITypeSymbol, (BindabilityMethod?, IMethodSymbol?)> BindabilityCache = new();
private static readonly BoundedCacheWithFactory<ITypeSymbol, (Parsability, ParsabilityMethod?)> ParsabilityCache = new();

private static bool IsTypeAlwaysParsable(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes, [NotNullWhen(true)] out ParsabilityMethod? parsabilityMethod)
{
// Any enum is valid.
Expand Down Expand Up @@ -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<MyType>()
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<IMethodSymbol>();
// MyType : IParsable<MyType>()
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<IMethodSymbol>();

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)
Expand Down Expand Up @@ -155,46 +163,54 @@ 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<IMethodSymbol>();

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)
{
return Bindability.Bindable;
}

// 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;
Expand Down