diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs index e48dd9445bfc..c05874bf536a 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Analyzers; @@ -248,4 +249,18 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Info, isEnabledByDefault: true, helpLinkUri: AnalyzersLink); + + internal static readonly DiagnosticDescriptor InvalidRouteConstraintForParameterType = CreateDescriptor( + "ASP0029", + Usage, + DiagnosticSeverity.Error); + + private static DiagnosticDescriptor CreateDescriptor(string id, string category, DiagnosticSeverity defaultSeverity, bool isEnabledByDefault = true, [CallerMemberName] string? name = null) => new( + id, + CreateLocalizableResourceString($"Analyzer_{name}_Title"), + CreateLocalizableResourceString($"Analyzer_{name}_Message"), + category, + defaultSeverity, + isEnabledByDefault, + helpLinkUri: AnalyzersLink); } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx index 8c9397f5be64..601155ef337f 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx @@ -333,4 +333,10 @@ If the server does not specifically reject IPv6, IPAddress.IPv6Any is preferred over IPAddress.Any usage for safety and performance reasons. See https://aka.ms/aspnetcore-warnings/ASP0028 for more details. + + Invalid constraint for parameter type + + + The constraint '{0}' on parameter '{1}' can't be used with type '{2}' + diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs index 274bf3d4d4ce..13b793790b73 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Concurrent; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; @@ -20,7 +22,8 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private const int DelegateParameterOrdinal = 2; - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + public override ImmutableArray SupportedDiagnostics { get; } = + [ DiagnosticDescriptors.DoNotUseModelBindingAttributesOnRouteHandlerParameters, DiagnosticDescriptors.DoNotReturnActionResultsFromRouteHandlers, DiagnosticDescriptors.DetectMisplacedLambdaAttribute, @@ -28,8 +31,9 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer DiagnosticDescriptors.RouteParameterComplexTypeIsNotParsable, DiagnosticDescriptors.BindAsyncSignatureMustReturnValueTaskOfT, DiagnosticDescriptors.AmbiguousRouteHandlerRoute, - DiagnosticDescriptors.AtMostOneFromBodyAttribute - ); + DiagnosticDescriptors.AtMostOneFromBodyAttribute, + DiagnosticDescriptors.InvalidRouteConstraintForParameterType + ]; public override void Initialize(AnalysisContext context) { @@ -74,15 +78,9 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary< return; } - IDelegateCreationOperation? delegateCreation = null; - foreach (var argument in invocation.Arguments) - { - if (argument.Parameter?.Ordinal == DelegateParameterOrdinal) - { - delegateCreation = argument.Descendants().OfType().FirstOrDefault(); - break; - } - } + // Already checked there are 3 arguments + var deleateArg = invocation.Arguments[DelegateParameterOrdinal]; + var delegateCreation = (IDelegateCreationOperation?)deleateArg.Descendants().FirstOrDefault(static d => d is IDelegateCreationOperation); if (delegateCreation is null) { @@ -100,6 +98,8 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary< return; } + AnalyzeRouteConstraints(routeUsage, wellKnownTypes, context); + mapOperations.TryAdd(MapOperation.Create(invocation, routeUsage), value: default); if (delegateCreation.Target.Kind == OperationKind.AnonymousFunction) @@ -172,23 +172,16 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary< private static bool TryGetStringToken(IInvocationOperation invocation, out SyntaxToken token) { - IArgumentOperation? argumentOperation = null; - foreach (var argument in invocation.Arguments) - { - if (argument.Parameter?.Ordinal == 1) - { - argumentOperation = argument; - } - } + var argumentOperation = invocation.Arguments[1]; - if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || - routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) + if (argumentOperation.Value is not ILiteralOperation literal) { token = default; return false; } - token = routePatternArgumentLiteralSyntax.Token; + var syntax = (LiteralExpressionSyntax)literal.Syntax; + token = syntax.Token; return true; } @@ -218,6 +211,133 @@ static bool IsCompatibleDelegateType(WellKnownTypes wellKnownTypes, IMethodSymbo } } + private static void AnalyzeRouteConstraints(RouteUsageModel routeUsage, WellKnownTypes wellKnownTypes, OperationAnalysisContext context) + { + foreach (var routeParam in routeUsage.RoutePattern.RouteParameters) + { + var handlerParam = GetHandlerParam(routeParam.Name, routeUsage); + + if (handlerParam is null) + { + continue; + } + + foreach (var policy in routeParam.Policies) + { + if (IsConstraintInvalidForType(policy, handlerParam.Type, wellKnownTypes)) + { + var descriptor = DiagnosticDescriptors.InvalidRouteConstraintForParameterType; + var start = routeParam.Span.Start + routeParam.Name.Length + 2; // including '{' and ':' + var textSpan = new TextSpan(start, routeParam.Span.End - start - 1); // excluding '}' + var location = Location.Create(context.FilterTree, textSpan); + var diagnostic = Diagnostic.Create(descriptor, location, policy.AsMemory(1), routeParam.Name, handlerParam.Type.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } + } + } + + private static bool IsConstraintInvalidForType(string policy, ITypeSymbol type, WellKnownTypes wellKnownTypes) + { + if (policy.EndsWith(")", StringComparison.Ordinal)) // Parameterized constraint + { + var braceIndex = policy.IndexOf('('); + + if (braceIndex == -1) + { + return false; + } + + var constraint = policy.AsSpan(1, braceIndex - 1); + + return constraint switch + { + "length" or "minlength" or "maxlength" or "regex" when type.SpecialType is not SpecialType.System_String => true, + "min" or "max" or "range" when !IsIntegerType(type) && !IsNullableIntegerType(type) => true, + _ => false + }; + } + else // Simple constraint + { + var constraint = policy.AsSpan(1); + + return constraint switch + { + "int" when !IsIntegerType(type) && !IsNullableIntegerType(type) => true, + "bool" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Boolean) => true, + "datetime" when !IsValueTypeOrNullableValueType(type, SpecialType.System_DateTime) => true, + "double" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Double) => true, + "guid" when !IsGuidType(type, wellKnownTypes) && !IsNullableGuidType(type, wellKnownTypes) => true, + "long" when !IsLongType(type) && !IsNullableLongType(type) => true, + "decimal" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Decimal) => true, + "float" when !IsValueTypeOrNullableValueType(type, SpecialType.System_Single) => true, + "alpha" when type.SpecialType is not SpecialType.System_String => true, + "file" or "nonfile" when type.SpecialType is not SpecialType.System_String => true, + _ => false + }; + } + } + + private static IParameterSymbol? GetHandlerParam(string name, RouteUsageModel routeUsage) + { + foreach (var param in routeUsage.UsageContext.Parameters) + { + if (param.Name.Equals(name, StringComparison.Ordinal)) + { + return (IParameterSymbol)param; + } + } + + return null; + } + + private static bool IsGuidType(ITypeSymbol type, WellKnownTypes wellKnownTypes) + { + return type.Equals(wellKnownTypes.Get(WellKnownType.System_Guid), SymbolEqualityComparer.Default); + } + + private static bool IsIntegerType(ITypeSymbol type) + { + return type.SpecialType >= SpecialType.System_SByte && type.SpecialType <= SpecialType.System_UInt64; + } + + private static bool IsLongType(ITypeSymbol type) + { + return type.SpecialType is SpecialType.System_Int64 or SpecialType.System_UInt64; + } + + private static bool IsNullableGuidType(ITypeSymbol type, WellKnownTypes wellKnownTypes) + { + return IsNullableType(type, out var namedType) && IsGuidType(namedType.TypeArguments[0], wellKnownTypes); + } + + private static bool IsNullableIntegerType(ITypeSymbol type) + { + return IsNullableType(type, out var namedType) && IsIntegerType(namedType.TypeArguments[0]); + } + + private static bool IsNullableLongType(ITypeSymbol type) + { + return IsNullableType(type, out var namedType) && IsLongType(namedType.TypeArguments[0]); + } + + public static bool IsNullableType(ITypeSymbol type, [NotNullWhen(true)] out INamedTypeSymbol? namedType) + { + namedType = type as INamedTypeSymbol; + return namedType != null && namedType.ConstructedFrom.SpecialType == SpecialType.System_Nullable_T; + } + + private static bool IsNullableValueType(ITypeSymbol type, SpecialType specialType) + { + return IsNullableType(type, out var namedType) && namedType.TypeArguments[0].SpecialType == specialType; + } + + private static bool IsValueTypeOrNullableValueType(ITypeSymbol type, SpecialType specialType) + { + return type.SpecialType == specialType || IsNullableValueType(type, specialType); + } + private record struct MapOperation(IOperation? Builder, IInvocationOperation Operation, RouteUsageModel RouteUsageModel) { public static MapOperation Create(IInvocationOperation operation, RouteUsageModel routeUsageModel) diff --git a/src/Framework/AspNetCoreAnalyzers/test/Extensions/CSharpAnalyzerTestExtensions.cs b/src/Framework/AspNetCoreAnalyzers/test/Extensions/CSharpAnalyzerTestExtensions.cs new file mode 100644 index 000000000000..a341d4758b9d --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/Extensions/CSharpAnalyzerTestExtensions.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Analyzers.Verifiers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace Microsoft.AspNetCore.Analyzers; + +public static class CSharpAnalyzerTestExtensions +{ + extension(CSharpAnalyzerTest) + where TAnalyzer : DiagnosticAnalyzer, new() + where TVerifier : IVerifier, new() + { + public static CSharpAnalyzerTest Create([StringSyntax("C#-test")] string source, params ReadOnlySpan expectedDiagnostics) + { + var test = new CSharpAnalyzerTest + { + TestCode = source.ReplaceLineEndings(), + // We need to set the output type to an exe to properly + // support top-level programs in the tests. Otherwise, + // the test infra will assume we are trying to build a library. + TestState = { OutputKind = OutputKind.ConsoleApplication }, + ReferenceAssemblies = CSharpAnalyzerVerifier.GetReferenceAssemblies(), + }; + + test.ExpectedDiagnostics.AddRange(expectedDiagnostics); + return test; + } + } + + public static CSharpAnalyzerTest WithSource(this CSharpAnalyzerTest test, [StringSyntax("C#-test")] string source) + where TAnalyzer : DiagnosticAnalyzer, new() + where TVerifier : IVerifier, new() + { + test.TestState.Sources.Add(source); + return test; + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs index ab0c98cae9e0..cec926d5fe9c 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/DetectMismatchedParameterOptionalityTest.cs @@ -300,7 +300,7 @@ public async Task OptionalRouteParamRequiredArgument_WithRegexConstraint_Produce using Microsoft.AspNetCore.Builder; var app = WebApplication.Create(); -app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", ({|#0:int age|}) => $""Age: {age}""); +app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", ({|#0:string age|}) => $""Age: {age}""); "; var fixedSource = @" @@ -308,7 +308,7 @@ public async Task OptionalRouteParamRequiredArgument_WithRegexConstraint_Produce using Microsoft.AspNetCore.Builder; var app = WebApplication.Create(); -app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", (int? age) => $""Age: {age}""); +app.MapGet(""/hello/{age:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)?}"", (string? age) => $""Age: {age}""); "; var expectedDiagnostic = new DiagnosticResult(DiagnosticDescriptors.DetectMismatchedParameterOptionality).WithArguments("age").WithLocation(0); diff --git a/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/InvalidRouteConstraintForParameterType.cs b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/InvalidRouteConstraintForParameterType.cs new file mode 100644 index 000000000000..988dc0544c09 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/RouteHandlers/InvalidRouteConstraintForParameterType.cs @@ -0,0 +1,355 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Data; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Testing; + +using CSTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerTest< + Microsoft.AspNetCore.Analyzers.RouteHandlers.RouteHandlerAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; + +public class InvalidRouteConstraintForParameterType +{ + private const string Program = $$$""" + using Microsoft.AspNetCore.Builder; + + var webApp = WebApplication.Create(); + """; + + private static string[] IntConstraints = ["int", "min(10)", "max(10)", "range(1,10)"]; + private static string[] IntTypes = ["byte", "sbyte", "short", "ushort", "int", "uint", "long", "ulong"]; + + public static TheoryData MapMethods { get; } = ["Map", "MapDelete", "MapFallback", "MapGet", "MapPatch", "MapPost", "MapPut"]; + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task LambdaWithValidConstraint_NoDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetValidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i++}}} + { + public static void Map(WebApplication app) + { + app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", ({{{type}}} param) => { }); + } + } + """); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task LambdaWithInvalidConstraint_HasDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetInvalidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i}}} + { + public static void Map(WebApplication app) + { + app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", ({{{type}}} param) => { }); + } + } + """); + + test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++)); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task LocalFunctionWithValidConstraint_NoDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetValidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i++}}} + { + public static void Map(WebApplication app) + { + app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", LocalFunction); + + string LocalFunction({{{type}}} param) => param.ToString(); + } + } + """); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task LocalFunctionWithInvalidConstraint_HasDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetInvalidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i}}} + { + public static void Map(WebApplication app) + { + app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", LocalFunction); + + string LocalFunction({{{type}}} param) => param.ToString(); + } + } + """); + + test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++)); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task InstanceMethodWithValidConstraint_NoDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetValidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i++}}} + { + public static void Map(WebApplication app) + { + var handler = new Handler(); + app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", handler.Handle); + } + + private class Handler + { + public string Handle({{{type}}} param) => param.ToString(); + } + } + """); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task InstanceMethodWithInvalidConstraint_HasDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetInvalidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i}}} + { + public static void Map(WebApplication app) + { + var handler = new Handler(); + app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", handler.Handle); + } + + private class Handler + { + public string Handle({{{type}}} param) => param.ToString(); + } + } + """); + + test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++)); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task StaticMethodWithValidConstraint_NoDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetValidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i++}}} + { + public static void Map(WebApplication app) + { + app.{{{methodName}}}(@"/api/{param:{{{constraint}}}}", Handler.Handle); + } + + private static class Handler + { + public static string Handle({{{type}}} param) => param.ToString(); + } + } + """); + } + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [MemberData(nameof(MapMethods))] + public async Task StaticMethodWithInvalidConstraint_HasDiagnostics(string methodName) + { + // Arrange + var test = CSTest.Create(Program); + var i = 0; + + foreach (var (constraint, type) in GetInvalidCombinations()) + { + test.WithSource($$$""" + using System; + using Microsoft.AspNetCore.Builder; + + public static class Endpoints{{{i}}} + { + public static void Map(WebApplication app) + { + app.{{{methodName}}}(@"/api/{param:{|#{{{i}}}:{{{constraint}}}|}}", Handler.Handle); + } + + public static class Handler + { + public static string Handle({{{type}}} param) => param.ToString(); + } + } + """); + + test.ExpectedDiagnostics.Add(CreateDiagnostic(constraint, "param", type, location: i++)); + } + + // Act & Assert + await test.RunAsync(); + } + + public static IEnumerable<(string constraint, string type)> GetValidCombinations() + { + yield return ("bool", "bool"); + yield return ("datetime", "DateTime"); + yield return ("decimal", "decimal"); + yield return ("double", "double"); + yield return ("float", "float"); + yield return ("guid", "Guid"); + + yield return ("alpha", "string"); + yield return ("file", "string"); + yield return ("nonfile", "string"); + + yield return ("length(10)", "string"); + yield return ("minlength(10)", "string"); + yield return ("maxlength(10)", "string"); + yield return (@"regex(\w+)", "string"); + + yield return ("long", "long"); + yield return ("long", "ulong"); + + foreach (var constraint in IntConstraints) + { + foreach (var type in IntTypes) + { + yield return (constraint, type); + } + } + } + + public static IEnumerable<(string constraint, string type)> GetInvalidCombinations() + { + yield return ("bool", "int"); + yield return ("datetime", "int"); + yield return ("decimal", "int"); + yield return ("double", "int"); + yield return ("float", "int"); + yield return ("guid", "int"); + + yield return ("alpha", "int"); + yield return ("file", "int"); + yield return ("nonfile", "int"); + + yield return ("length(10)", "int"); + yield return ("minlength(10)", "int"); + yield return ("maxlength(10)", "int"); + yield return (@"regex(\w+)", "int"); + + yield return ("long", "byte"); + yield return ("long", "sbyte"); + yield return ("long", "short"); + yield return ("long", "ushort"); + yield return ("long", "int"); + yield return ("long", "uint"); + + foreach (var constraint in IntConstraints) + { + yield return (constraint, "string"); + } + } + + private static DiagnosticResult CreateDiagnostic(string constraint, string parameter, string typeName, int location = 0) + { + return new DiagnosticResult(DiagnosticDescriptors.InvalidRouteConstraintForParameterType) + .WithArguments(constraint, parameter, typeName) + .WithLocation(location); + } +} diff --git a/src/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs index a64dd1a426e8..981fd3d50150 100644 --- a/src/Shared/RoslynUtils/WellKnownTypeData.cs +++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs @@ -35,6 +35,7 @@ public enum WellKnownType System_Collections_IEnumerable, System_DateOnly, System_DateTimeOffset, + System_Guid, System_IO_Stream, System_IO_Pipelines_PipeReader, System_IFormatProvider, @@ -159,6 +160,7 @@ public enum WellKnownType "System.Collections.IEnumerable", "System.DateOnly", "System.DateTimeOffset", + "System.Guid", "System.IO.Stream", "System.IO.Pipelines.PipeReader", "System.IFormatProvider",