diff --git a/src/DocumentationGenerator/Program.cs b/src/DocumentationGenerator/Program.cs index eaae9815..945a6d95 100644 --- a/src/DocumentationGenerator/Program.cs +++ b/src/DocumentationGenerator/Program.cs @@ -15,7 +15,6 @@ Console.WriteLine("Cannot find the current git folder"); return 1; } - var fileWritten = 0; var assemblies = new[] { typeof(Meziantou.Analyzer.Rules.CommaAnalyzer).Assembly, typeof(Meziantou.Analyzer.Rules.CommaFixer).Assembly }; diff --git a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs index 44c2a753..96d5ef5b 100644 --- a/src/Meziantou.Analyzer/Internals/OverloadFinder.cs +++ b/src/Meziantou.Analyzer/Internals/OverloadFinder.cs @@ -7,6 +7,8 @@ namespace Meziantou.Analyzer.Internals; internal sealed class OverloadFinder(Compilation compilation) { private readonly ITypeSymbol? _obsoleteSymbol = compilation.GetBestTypeByMetadataName("System.ObsoleteAttribute"); + private readonly INamedTypeSymbol? _ienumerableOfTSymbol = compilation.GetBestTypeByMetadataName("System.Collections.Generic.IEnumerable`1"); + private readonly INamedTypeSymbol? _halfSymbol = compilation.GetBestTypeByMetadataName("System.Half"); private static ReadOnlySpan Wrap(ReadOnlySpan types) { @@ -105,35 +107,43 @@ public bool HasOverloadWithAdditionalParameterOfType(IMethodSymbol methodSymbol, if (additionalParameterTypes.IsEmpty) return null; - ImmutableArray members; - if (options.SyntaxNode is not null) + foreach (var method in FindSimilarMethods(methodSymbol, options, methodSymbol.Name, additionalParameterTypes)) { - var semanticModel = compilation.GetSemanticModel(options.SyntaxNode.SyntaxTree); - members = semanticModel.LookupSymbols(options.SyntaxNode.GetLocation().SourceSpan.End, methodSymbol.ContainingType, methodSymbol.Name, includeReducedExtensionMethods: true); - } - else - { - members = methodSymbol.ContainingType.GetMembers(methodSymbol.Name); + return method; } + return null; + } + + public ImmutableArray FindSimilarMethods(IMethodSymbol methodSymbol, OverloadOptions options, string methodName, ReadOnlySpan additionalParameterTypes) + { + additionalParameterTypes = RemoveNulls(additionalParameterTypes); + + var result = new List(); + var members = GetCandidateMethods(methodSymbol, methodName, options); foreach (var member in members) { - if (member is IMethodSymbol method) - { - if (!options.IncludeObsoleteMembers && IsObsolete(method)) - continue; + if (member is not IMethodSymbol method) + continue; - if (HasSimilarParameters(methodSymbol, method, options.AllowOptionalParameters, additionalParameterTypes)) - return method; + if (!options.IncludeObsoleteMembers && IsObsolete(method)) + continue; + + if (options.ShouldCheckMethod is not null && !options.ShouldCheckMethod(method)) + continue; + + if (HasSimilarParametersCore(methodSymbol, method, options, additionalParameterTypes)) + { + result.Add(method); } } - return null; + return ImmutableArray.CreateRange(result); } - public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ReadOnlySpan additionalParameterTypes) + public bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ReadOnlySpan additionalParameterTypes) { - return HasSimilarParameters(method, otherMethod, allowOptionalParameters, Wrap(additionalParameterTypes)); + return HasSimilarParameters(method, otherMethod, new OverloadOptions(AllowOptionalParameters: allowOptionalParameters), Wrap(additionalParameterTypes)); } /// @@ -145,29 +155,51 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe /// If , can have more parameters if they are optional /// /// - public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ReadOnlySpan additionalParameterTypes) + public bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, bool allowOptionalParameters, params ReadOnlySpan additionalParameterTypes) + { + return HasSimilarParameters(method, otherMethod, new OverloadOptions(AllowOptionalParameters: allowOptionalParameters), additionalParameterTypes); + } + + public bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, OverloadOptions options, params ReadOnlySpan additionalParameterTypes) + { + return HasSimilarParametersCore(method, otherMethod, options, Wrap(additionalParameterTypes)); + } + + public bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol otherMethod, OverloadOptions options, params ReadOnlySpan additionalParameterTypes) + { + return HasSimilarParametersCore(method, otherMethod, options, additionalParameterTypes); + } + + private bool HasSimilarParametersCore(IMethodSymbol method, IMethodSymbol otherMethod, OverloadOptions options, ReadOnlySpan additionalParameterTypes) { if (method.IsEqualTo(otherMethod)) return false; - // The new method must have at least the same number of parameters as the old method, plus the number of additional parameters - if (otherMethod.Parameters.Length - method.Parameters.Length < additionalParameterTypes.Length) + if (!HaveCompatibleGenericSignatures(method, otherMethod)) + return false; + + var methodParameters = GetComparableParameters(method, otherMethod); + var otherMethodParameters = GetComparableParameters(otherMethod, method); + + // The new method must have at least the same number of parameters as the old method, plus the number of additional parameters + if (otherMethodParameters.Length - methodParameters.Length < additionalParameterTypes.Length) return false; // If allowOptionalParameters is false, the new method must have exactly the same number of parameters as the old method - if (!allowOptionalParameters && otherMethod.Parameters.Length - method.Parameters.Length != additionalParameterTypes.Length) + if (!options.AllowOptionalParameters && otherMethodParameters.Length - methodParameters.Length != additionalParameterTypes.Length) return false; // Most of the time, an overload has the same order for the parameters. Try to match them in order first (faster) { + var inferredMethodTypeArguments = new Dictionary(SymbolEqualityComparer.Default); int i = 0, j = 0; var additionalParameterIndex = 0; - while (i < method.Parameters.Length && j < otherMethod.Parameters.Length) + while (i < methodParameters.Length && j < otherMethodParameters.Length) { - var methodParameter = method.Parameters[i]; - var otherMethodParameter = otherMethod.Parameters[j]; + var methodParameter = methodParameters[i]; + var otherMethodParameter = otherMethodParameters[j]; - if (methodParameter.IsEqualTo(otherMethodParameter)) + if (AreParametersCompatible(methodParameter, otherMethodParameter, method, otherMethod, options, _ienumerableOfTSymbol, _halfSymbol, inferredMethodTypeArguments)) { i++; j++; @@ -193,23 +225,24 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe break; } - if (i == method.Parameters.Length && j == otherMethod.Parameters.Length && additionalParameterIndex == additionalParameterTypes.Length) - return true; + if (i == methodParameters.Length && j == otherMethodParameters.Length && additionalParameterIndex == additionalParameterTypes.Length) + return AreInferredGenericConstraintsSatisfied(method, otherMethod, inferredMethodTypeArguments); } // Slower search, allows to find overload with different parameter order // Also, handle allow optional parameters { - var otherMethodParameters = otherMethod.Parameters; + var inferredMethodTypeArguments = new Dictionary(SymbolEqualityComparer.Default); + var unmatchedOtherMethodParameters = otherMethodParameters; - foreach (var param in method.Parameters) + foreach (var param in methodParameters) { var found = false; - for (var i = 0; i < otherMethodParameters.Length; i++) + for (var i = 0; i < unmatchedOtherMethodParameters.Length; i++) { - if (otherMethodParameters[i].Type.IsEqualTo(param.Type)) + if (AreParametersCompatible(param, unmatchedOtherMethodParameters[i], method, otherMethod, options, _ienumerableOfTSymbol, _halfSymbol, inferredMethodTypeArguments)) { - otherMethodParameters = otherMethodParameters.RemoveAt(i); + unmatchedOtherMethodParameters = unmatchedOtherMethodParameters.RemoveAt(i); found = true; break; } @@ -222,11 +255,11 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe foreach (var paramType in additionalParameterTypes) { var found = false; - for (var i = 0; i < otherMethodParameters.Length; i++) + for (var i = 0; i < unmatchedOtherMethodParameters.Length; i++) { - if (IsEqualTo(otherMethodParameters[i].Type, paramType)) + if (IsEqualTo(unmatchedOtherMethodParameters[i].Type, paramType)) { - otherMethodParameters = otherMethodParameters.RemoveAt(i); + unmatchedOtherMethodParameters = unmatchedOtherMethodParameters.RemoveAt(i); found = true; break; } @@ -236,12 +269,12 @@ public static bool HasSimilarParameters(IMethodSymbol method, IMethodSymbol othe return false; } - if (otherMethodParameters.Length == 0) - return true; + if (unmatchedOtherMethodParameters.Length == 0) + return AreInferredGenericConstraintsSatisfied(method, otherMethod, inferredMethodTypeArguments); - if (allowOptionalParameters) + if (options.AllowOptionalParameters) { - if (otherMethodParameters.All(p => p.IsOptional)) + if (unmatchedOtherMethodParameters.All(p => p.IsOptional)) return true; } @@ -254,6 +287,370 @@ static bool IsEqualTo(ITypeSymbol left, OverloadParameterType right) ? left.IsOrInheritFrom(right.Symbol) : left.IsEqualTo(right.Symbol); } + + static bool HaveCompatibleGenericSignatures(IMethodSymbol method, IMethodSymbol otherMethod) + { + if (method.IsGenericMethod && !otherMethod.IsGenericMethod) + return false; + + if (!method.IsGenericMethod) + return true; + + if (method.Arity != otherMethod.Arity) + return false; + + for (var i = 0; i < method.Arity; i++) + { + var methodTypeParameter = method.TypeParameters[i]; + var otherMethodTypeParameter = otherMethod.TypeParameters[i]; + + if (methodTypeParameter.HasReferenceTypeConstraint != otherMethodTypeParameter.HasReferenceTypeConstraint || + methodTypeParameter.HasValueTypeConstraint != otherMethodTypeParameter.HasValueTypeConstraint || + methodTypeParameter.HasNotNullConstraint != otherMethodTypeParameter.HasNotNullConstraint || + methodTypeParameter.HasUnmanagedTypeConstraint != otherMethodTypeParameter.HasUnmanagedTypeConstraint || + methodTypeParameter.HasConstructorConstraint != otherMethodTypeParameter.HasConstructorConstraint || + methodTypeParameter.Variance != otherMethodTypeParameter.Variance || + methodTypeParameter.ConstraintTypes.Length != otherMethodTypeParameter.ConstraintTypes.Length) + { + return false; + } + + for (var j = 0; j < methodTypeParameter.ConstraintTypes.Length; j++) + { + if (!methodTypeParameter.ConstraintTypes[j].IsEqualTo(otherMethodTypeParameter.ConstraintTypes[j])) + return false; + } + } + + return true; + } + + static bool AreParametersCompatible(IParameterSymbol methodParameter, IParameterSymbol otherMethodParameter, IMethodSymbol method, IMethodSymbol otherMethod, OverloadOptions options, ITypeSymbol? ienumerableOfTSymbol, ITypeSymbol? halfSymbol, Dictionary inferredMethodTypeArguments) + { + if (!options.AllowParamsToNonParamsCompatibility && methodParameter.IsParams != otherMethodParameter.IsParams) + return false; + + if (!AreRefKindsCompatible(methodParameter.RefKind, otherMethodParameter.RefKind, options)) + return false; + + return AreTypesCompatible(methodParameter.Type, otherMethodParameter.Type, method, otherMethod, options, ienumerableOfTSymbol, halfSymbol, inferredMethodTypeArguments); + } + + static bool AreRefKindsCompatible(RefKind methodRefKind, RefKind otherMethodRefKind, OverloadOptions options) + { + var methodIsByRef = methodRefKind is RefKind.Ref or RefKind.Out; + var otherMethodIsByRef = otherMethodRefKind is RefKind.Ref or RefKind.Out; + + if (methodIsByRef || otherMethodIsByRef) + return methodRefKind == otherMethodRefKind; + + if (!options.AllowInModifierCompatibility && (methodRefKind is RefKind.In || otherMethodRefKind is RefKind.In)) + return methodRefKind == otherMethodRefKind; + + // `in` and by-value calls should be treated as compatible for analyzer matching. + return true; + } + + static bool AreTypesCompatible(ITypeSymbol methodType, ITypeSymbol otherMethodType, IMethodSymbol method, IMethodSymbol otherMethod, OverloadOptions options, ITypeSymbol? ienumerableOfTSymbol, ITypeSymbol? halfSymbol, Dictionary inferredMethodTypeArguments) + { + if (methodType.IsEqualTo(otherMethodType)) + return true; + + if (TryGetMethodTypeArgument(otherMethodType, method, otherMethod, out var mappedType)) + return methodType.IsEqualTo(mappedType); + + if (options.AllowNumericConversion && IsSafeImplicitNumericConversion(methodType, otherMethodType, halfSymbol)) + return true; + + if (methodType is IArrayTypeSymbol methodArrayType && + otherMethodType is IArrayTypeSymbol otherMethodArrayType && + methodArrayType.Rank == otherMethodArrayType.Rank) + { + return AreTypesCompatible(methodArrayType.ElementType, otherMethodArrayType.ElementType, method, otherMethod, options, ienumerableOfTSymbol, halfSymbol, inferredMethodTypeArguments); + } + + if (methodType is not INamedTypeSymbol methodNamedType || otherMethodType is not INamedTypeSymbol otherMethodNamedType) + return false; + + if (methodNamedType.ConstructedFrom.IsEqualTo(otherMethodNamedType.ConstructedFrom)) + { + if (methodNamedType.TypeArguments.Length != otherMethodNamedType.TypeArguments.Length) + return false; + + for (var i = 0; i < methodNamedType.TypeArguments.Length; i++) + { + var methodTypeArgument = methodNamedType.TypeArguments[i]; + var otherMethodTypeArgument = otherMethodNamedType.TypeArguments[i]; + + if (TryGetMethodTypeArgument(otherMethodTypeArgument, method, otherMethod, out mappedType)) + { + if (!methodTypeArgument.IsEqualTo(mappedType)) + return false; + + continue; + } + + if (!methodTypeArgument.IsEqualTo(otherMethodTypeArgument)) + return false; + } + + return true; + } + + if (IsIEnumerableType(otherMethodNamedType.OriginalDefinition, ienumerableOfTSymbol)) + return false; + + if (options.AllowInterfaceConversions) + { + foreach (var candidate in methodNamedType.GetAllInterfacesIncludingThis().OfType()) + { + if (!candidate.OriginalDefinition.IsEqualTo(otherMethodNamedType.OriginalDefinition)) + continue; + + if (candidate.TypeArguments.Length != otherMethodNamedType.TypeArguments.Length) + continue; + + var isCompatible = true; + for (var i = 0; i < candidate.TypeArguments.Length; i++) + { + var sourceTypeArgument = candidate.TypeArguments[i]; + var targetTypeArgument = otherMethodNamedType.TypeArguments[i]; + if (!AreGenericTypeArgumentsCompatible(sourceTypeArgument, targetTypeArgument, method, otherMethod, inferredMethodTypeArguments)) + { + isCompatible = false; + break; + } + } + + if (isCompatible) + return true; + } + } + + if (methodNamedType is INamedTypeSymbol directCandidate && + directCandidate.BaseType is INamedTypeSymbol baseTypeCandidate && + baseTypeCandidate.OriginalDefinition.IsEqualTo(otherMethodNamedType.OriginalDefinition) && + baseTypeCandidate.TypeArguments.Length == otherMethodNamedType.TypeArguments.Length) + { + var isCompatible = true; + for (var i = 0; i < baseTypeCandidate.TypeArguments.Length; i++) + { + if (!AreGenericTypeArgumentsCompatible(baseTypeCandidate.TypeArguments[i], otherMethodNamedType.TypeArguments[i], method, otherMethod, inferredMethodTypeArguments)) + { + isCompatible = false; + break; + } + } + + if (isCompatible) + return true; + } + + return false; + } + + static bool IsIEnumerableType(INamedTypeSymbol typeSymbol, ITypeSymbol? ienumerableOfTSymbol) + { + return ienumerableOfTSymbol is not null && typeSymbol.OriginalDefinition.IsEqualTo(ienumerableOfTSymbol); + } + + static bool AreGenericTypeArgumentsCompatible(ITypeSymbol sourceTypeArgument, ITypeSymbol targetTypeArgument, IMethodSymbol method, IMethodSymbol otherMethod, Dictionary inferredMethodTypeArguments) + { + if (TryGetMethodTypeArgument(targetTypeArgument, method, otherMethod, out var mappedType)) + return sourceTypeArgument.IsEqualTo(mappedType); + + if (targetTypeArgument is ITypeParameterSymbol + { + TypeParameterKind: TypeParameterKind.Method, + ContainingSymbol: IMethodSymbol containingMethod, + } typeParameter + && containingMethod.IsEqualTo(otherMethod)) + { + if (inferredMethodTypeArguments.TryGetValue(typeParameter, out var inferredTypeArgument)) + return sourceTypeArgument.IsEqualTo(inferredTypeArgument); + + inferredMethodTypeArguments[typeParameter] = sourceTypeArgument; + return true; + } + + return sourceTypeArgument.IsEqualTo(targetTypeArgument); + } + + static bool AreInferredGenericConstraintsSatisfied(IMethodSymbol sourceMethod, IMethodSymbol targetMethod, Dictionary inferredMethodTypeArguments) + { + if (!targetMethod.IsGenericMethod) + return true; + + foreach (var typeParameter in targetMethod.TypeParameters) + { + ITypeSymbol? inferredTypeArgument = null; + if (!inferredMethodTypeArguments.TryGetValue(typeParameter, out inferredTypeArgument)) + { + if (sourceMethod.IsGenericMethod && + sourceMethod.Arity == targetMethod.Arity && + typeParameter.Ordinal < sourceMethod.TypeArguments.Length) + { + inferredTypeArgument = sourceMethod.TypeArguments[typeParameter.Ordinal]; + } + else + { + return false; + } + } + + if (typeParameter.HasReferenceTypeConstraint && !inferredTypeArgument.IsReferenceType) + return false; + + if (typeParameter.HasValueTypeConstraint && !inferredTypeArgument.IsValueType) + return false; + + if (typeParameter.HasUnmanagedTypeConstraint && !inferredTypeArgument.IsUnmanagedType) + return false; + + if (typeParameter.HasConstructorConstraint && + !inferredTypeArgument.IsValueType && + inferredTypeArgument is INamedTypeSymbol namedType && + !namedType.InstanceConstructors.Any(ctor => ctor.Parameters.Length == 0 && ctor.DeclaredAccessibility == Accessibility.Public)) + { + return false; + } + + foreach (var constraintType in typeParameter.ConstraintTypes) + { + if (!inferredTypeArgument.IsOrInheritFrom(constraintType) && !inferredTypeArgument.Implements(constraintType)) + return false; + } + } + + return true; + } + + static bool IsSafeImplicitNumericConversion(ITypeSymbol sourceType, ITypeSymbol targetType, ITypeSymbol? halfSymbol) + { + if (sourceType is INamedTypeSymbol namedType && + IsMetadataType(namedType, halfSymbol) && + targetType.SpecialType is SpecialType.System_Single or SpecialType.System_Double) + { + return true; + } + + return (sourceType.SpecialType, targetType.SpecialType) switch + { + (SpecialType.System_SByte, SpecialType.System_Int16 or SpecialType.System_Int32 or SpecialType.System_Int64 or SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_Byte, SpecialType.System_Int16 or SpecialType.System_UInt16 or SpecialType.System_Int32 or SpecialType.System_UInt32 or SpecialType.System_Int64 or SpecialType.System_UInt64 or SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_Int16, SpecialType.System_Int32 or SpecialType.System_Int64 or SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_UInt16, SpecialType.System_Int32 or SpecialType.System_UInt32 or SpecialType.System_Int64 or SpecialType.System_UInt64 or SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_Int32, SpecialType.System_Int64 or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_UInt32, SpecialType.System_Int64 or SpecialType.System_UInt64 or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_Int64, SpecialType.System_Decimal) => true, + (SpecialType.System_UInt64, SpecialType.System_Decimal) => true, + (SpecialType.System_Char, SpecialType.System_Int32 or SpecialType.System_UInt32 or SpecialType.System_Int64 or SpecialType.System_UInt64 or SpecialType.System_Single or SpecialType.System_Double or SpecialType.System_Decimal) => true, + (SpecialType.System_Single, SpecialType.System_Double) => true, + _ => false, + }; + } + + static bool TryGetMethodTypeArgument(ITypeSymbol typeSymbol, IMethodSymbol method, IMethodSymbol otherMethod, [NotNullWhen(true)] out ITypeSymbol? mappedType) + { + if (typeSymbol is ITypeParameterSymbol + { + TypeParameterKind: TypeParameterKind.Method, + ContainingSymbol: IMethodSymbol containingMethodSymbol, + } typeParameter + && containingMethodSymbol.IsEqualTo(otherMethod) + && typeParameter.Ordinal < method.TypeArguments.Length) + { + mappedType = method.TypeArguments[typeParameter.Ordinal]; + return true; + } + + mappedType = null; + return false; + } + + static bool IsMetadataType(INamedTypeSymbol typeSymbol, ITypeSymbol? expectedType) + { + return expectedType is not null && typeSymbol.OriginalDefinition.IsEqualTo(expectedType); + } + + static ImmutableArray GetComparableParameters(IMethodSymbol method, IMethodSymbol otherMethod) + { + if (method.MethodKind is MethodKind.ReducedExtension && + method.ReducedFrom is { Parameters.Length: > 0 } reducedFrom) + { + return reducedFrom.Parameters.RemoveAt(0); + } + + if (method.IsExtensionMethod && + method.Parameters.Length > 0 && + !otherMethod.IsStatic && + method.Parameters[0].Type.IsEqualTo(otherMethod.ContainingType)) + { + return method.Parameters.RemoveAt(0); + } + + return method.Parameters; + } + } + + private ImmutableArray GetCandidateMethods(IMethodSymbol methodSymbol, string methodName, OverloadOptions options) + { + if (methodSymbol.ContainingType is null) + return ImmutableArray.Empty; + + var results = new List(); + var knownSymbols = new HashSet(SymbolEqualityComparer.Default); + + static void AddSymbols(IEnumerable symbols, List results, HashSet knownSymbols) + { + foreach (var symbol in symbols) + { + if (knownSymbols.Add(symbol)) + { + results.Add(symbol); + } + } + } + + var reducedReceiverType = GetReducedReceiverType(methodSymbol); + if (options.SyntaxNode is not null) + { + var semanticModel = compilation.GetSemanticModel(options.SyntaxNode.SyntaxTree); + var position = options.SyntaxNode.GetLocation().SourceSpan.End; + + AddSymbols(semanticModel.LookupSymbols(position, methodSymbol.ContainingType, methodName, includeReducedExtensionMethods: true), results, knownSymbols); + if (reducedReceiverType is not null) + { + AddSymbols(semanticModel.LookupSymbols(position, reducedReceiverType, methodName, includeReducedExtensionMethods: false), results, knownSymbols); + AddSymbols(reducedReceiverType.GetMembers(methodName), results, knownSymbols); + } + } + else + { + AddSymbols(methodSymbol.ContainingType.GetMembers(methodName), results, knownSymbols); + if (reducedReceiverType is not null) + { + AddSymbols(reducedReceiverType.GetMembers(methodName), results, knownSymbols); + } + } + + return ImmutableArray.CreateRange(results); + } + + private static ITypeSymbol? GetReducedReceiverType(IMethodSymbol methodSymbol) + { + if (methodSymbol.MethodKind is MethodKind.ReducedExtension && + methodSymbol.ReducedFrom is { Parameters.Length: > 0 } reducedFromMethod) + { + return reducedFromMethod.Parameters[0].Type; + } + + if (methodSymbol.IsExtensionMethod && methodSymbol.Parameters.Length > 0) + { + return methodSymbol.Parameters[0].Type; + } + + return null; } private bool IsObsolete(IMethodSymbol methodSymbol) diff --git a/src/Meziantou.Analyzer/Internals/OverloadOptions.cs b/src/Meziantou.Analyzer/Internals/OverloadOptions.cs index 9d619e48..f3d50c83 100644 --- a/src/Meziantou.Analyzer/Internals/OverloadOptions.cs +++ b/src/Meziantou.Analyzer/Internals/OverloadOptions.cs @@ -1,5 +1,15 @@ +using System; using Microsoft.CodeAnalysis; namespace Meziantou.Analyzer.Internals; -internal record struct OverloadOptions(bool IncludeObsoleteMembers = false, bool AllowOptionalParameters = false, bool IncludeExtensionsMethods = false, SyntaxNode? SyntaxNode = null); +internal record struct OverloadOptions( + bool IncludeObsoleteMembers = false, + bool AllowOptionalParameters = false, + bool IncludeExtensionsMethods = false, + SyntaxNode? SyntaxNode = null, + bool AllowNumericConversion = true, + bool AllowParamsToNonParamsCompatibility = true, + bool AllowInModifierCompatibility = true, + bool AllowInterfaceConversions = true, + Func? ShouldCheckMethod = null); diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index 65ace740..c4ccc7a9 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -50,11 +50,11 @@ public override void Initialize(AnalysisContext context) }); } - private sealed class Context - { - private static readonly Version Version6 = new(6, 0, 0, 0); - - private readonly AwaitableTypes _awaitableTypes; + private sealed class Context + { + private static readonly Version Version6 = new(6, 0, 0, 0); + private readonly AwaitableTypes _awaitableTypes; + private readonly OverloadFinder _overloadFinder; private readonly INamedTypeSymbol[] _taskAwaiterLikeSymbols; private readonly ConcurrentHashSet _symbolsWithNoAsyncOverloads = new(SymbolEqualityComparer.Default); @@ -62,6 +62,7 @@ private sealed class Context public Context(Compilation compilation) { _awaitableTypes = new AwaitableTypes(compilation); + _overloadFinder = new OverloadFinder(compilation); var consoleSymbol = compilation.GetBestTypeByMetadataName("System.Console"); if (consoleSymbol is not null) @@ -261,70 +262,62 @@ private bool HasAsyncEquivalent(IInvocationOperation operation, [NotNullWhen(tru // Search async equivalent: sample.Write() => sample.WriteAsync() if (!targetMethod.ReturnType.OriginalDefinition.IsEqualToAny(TaskSymbol, TaskOfTSymbol)) { - var position = operation.Syntax.GetLocation().SourceSpan.End; - - var result = ProcessSymbols(operation.SemanticModel!.LookupSymbols(position, targetMethod.ContainingType, name: targetMethod.Name, includeReducedExtensionMethods: true)); - if (result is not null) + var asyncEquivalentMethod = FindPotentialAsyncEquivalent(operation, targetMethod, targetMethod.Name); + if (asyncEquivalentMethod is not null) { - data = result; + data = new($"Use '{asyncEquivalentMethod.Name}' instead of '{targetMethod.Name}'", DoNotUseBlockingCallInAsyncContextData.Overload, asyncEquivalentMethod.Name); return true; } if (!targetMethod.Name.EndsWith("Async", StringComparison.Ordinal)) { - result = ProcessSymbols(operation.SemanticModel!.LookupSymbols(position, targetMethod.ContainingType, name: targetMethod.Name + "Async", includeReducedExtensionMethods: true)); - if (result is not null) + asyncEquivalentMethod = FindPotentialAsyncEquivalent(operation, targetMethod, targetMethod.Name + "Async"); + if (asyncEquivalentMethod is not null) { - data = result; + data = new($"Use '{asyncEquivalentMethod.Name}' instead of '{targetMethod.Name}'", DoNotUseBlockingCallInAsyncContextData.Overload, asyncEquivalentMethod.Name); return true; } } - - DiagnosticData? ProcessSymbols(ImmutableArray potentialMethods) - { - foreach (var potentialMethod in potentialMethods) - { - if (IsPotentialMember(operation, targetMethod, potentialMethod)) - { - return new($"Use '{potentialMethod.Name}' instead of '{targetMethod.Name}'", DoNotUseBlockingCallInAsyncContextData.Overload, potentialMethod.Name); - } - } - - return null; - } } return false; } - private bool IsPotentialMember(IInvocationOperation operation, IMethodSymbol method, ISymbol potentialAsyncSymbol) + private IMethodSymbol? FindPotentialAsyncEquivalent(IInvocationOperation operation, IMethodSymbol targetMethod, string methodName) { - if (potentialAsyncSymbol.IsEqualTo(method)) - return false; + var options = new OverloadOptions( + AllowOptionalParameters: false, + IncludeExtensionsMethods: true, + SyntaxNode: operation.Syntax); - if (potentialAsyncSymbol is IMethodSymbol methodSymbol) + foreach (var candidateMethod in _overloadFinder.FindSimilarMethods(targetMethod, options, methodName, default)) { - if (method.IsStatic && !methodSymbol.IsStatic) - return false; - - if (!_awaitableTypes.IsAwaitable(methodSymbol.ReturnType, operation.SemanticModel!, operation.Syntax.SpanStart)) - return false; + if (IsPotentialAsyncEquivalent(operation, candidateMethod)) + return candidateMethod; + } - if (methodSymbol.HasAttribute(ObsoleteAttributeSymbol)) - return false; + if (CancellationTokenSymbol is not null) + { + foreach (var candidateMethod in _overloadFinder.FindSimilarMethods(targetMethod, options, methodName, [new OverloadParameterType(CancellationTokenSymbol)])) + { + if (IsPotentialAsyncEquivalent(operation, candidateMethod)) + return candidateMethod; + } + } - // In test projects, exclude async methods from Meziantou.Framework.TemporaryDirectory - if (IsTestProject && TemporaryDirectorySymbol is not null && methodSymbol.ContainingType.IsEqualTo(TemporaryDirectorySymbol)) - return false; + return null; + } - if (OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false, default(ReadOnlySpan))) - return true; + private bool IsPotentialAsyncEquivalent(IInvocationOperation operation, IMethodSymbol methodSymbol) + { + if (!_awaitableTypes.IsAwaitable(methodSymbol.ReturnType, operation.SemanticModel!, operation.Syntax.SpanStart)) + return false; - if (CancellationTokenSymbol is not null && OverloadFinder.HasSimilarParameters(method, methodSymbol, allowOptionalParameters: false, [CancellationTokenSymbol])) - return true; - } + // In test projects, exclude async methods from Meziantou.Framework.TemporaryDirectory + if (IsTestProject && TemporaryDirectorySymbol is not null && methodSymbol.ContainingType.IsEqualTo(TemporaryDirectorySymbol)) + return false; - return false; + return true; } private bool IsSemaphoreSlimWaitWithZeroTimeout(IInvocationOperation operation) diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index aa1635a8..d407bd65 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -784,16 +784,12 @@ await CreateProjectBuilder() class Test { - public void A() - { - } + public void A() => throw null; } static class TestExtensions { - public static async Task AAsync(this Test test, CancellationToken token = default) - { - } + public static async Task AAsync(this Test test, CancellationToken token = default) => throw null; } class demo @@ -812,23 +808,1008 @@ public async Task a() class Test { - public void A() + public void A() => throw null; + } + + static class TestExtensions + { + public static async Task AAsync(this Test test, CancellationToken token = default) => throw null; + } + + class demo + { + public async Task a() + { + await new Test().AAsync(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_MultipleIncompatibleGenericArguments_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(List a, List b) => throw null; + public Task AAsync(List a, List b, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() { + new Test().A(new List(), new List()); } } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ExtensionMethod_GenericArgumentsIncompatible_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + } static class TestExtensions { - public static async Task AAsync(this Test test, CancellationToken token = default) + public static void A(this Test test, List a, List b) => throw null; + public static Task AAsync(this Test test, List a, List b, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() { + new Test().A(new List(), new List()); } } + """) + .ValidateAsync(); + } - class demo + [Fact] + public async Task GenericArgument_ListToIEnumerable_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test { - public async Task a() + public void A(List value) => throw null; + public Task AAsync(IEnumerable value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() { - await new Test().AAsync(); + new Test().A(new List()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_NestedGenericIncompatibility_ShouldNotReport() + { + // For simplicity, we skip checking compatibility of nested generics + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(List> value) => throw null; + public Task AAsync(List> value, CancellationToken token = default) => throw null; + public Task AAsync(List> value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(new List>()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_SameOriginalDefinitionButDifferentTypeParameterMapping_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(Dictionary value) => throw null; + public Task AAsync(Dictionary value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(new Dictionary()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_SingleGenericArgument_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(List value) => throw null; + public Task AAsync(List value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(new List())|]; + } + } + """) + .ShouldFixCodeWith(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(List value) => throw null; + public Task AAsync(List value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + await new Test().AAsync(new List()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ExtensionMethod_GenericArgument_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + } + + static class TestExtensions + { + public static void A(this Test test, List value) => throw null; + public static Task AAsync(this Test test, List value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(new List())|]; + } + } + """) + .ShouldFixCodeWith(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + } + + static class TestExtensions + { + public static void A(this Test test, List value) => throw null; + public static Task AAsync(this Test test, List value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + await new Test().AAsync(new List()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_ArrayOfGenericArgument_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(T[] value) => throw null; + public Task AAsync(T[] value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(new int[1])|]; + } + } + """) + .ShouldFixCodeWith(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(T[] value) => throw null; + public Task AAsync(T[] value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + await new Test().AAsync(new int[1]); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_AsyncConstraintIncompatible_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(List value) => throw null; + public Task AAsync(List value, CancellationToken token = default) + where T : class => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(new List()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_InModifierDifference_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(in int value) => throw null; + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + var value = 1; + [|new Test().A(in value)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_RefMismatch_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(ref int value) => throw null; + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + var value = 1; + new Test().A(ref value); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_OutMismatch_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(out int value) + { + value = 0; + } + + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(out var value); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_NullLiteral_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(string value) => throw null; + public Task AAsync(string value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(null)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_DefaultLiteral_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(string value) => throw null; + public Task AAsync(string value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(default)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericConversion_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(long value) => throw null; + public Task AAsync(long value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(42)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericWidening_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(int value) => throw null; + public Task AAsync(long value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(1)|]; + } + } + """) + .ShouldFixCodeWith(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(int value) => throw null; + public Task AAsync(long value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + await new Test().AAsync(1); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericNarrowing_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(long value) => throw null; + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(1L); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericToFloatingPoint_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(int value) => throw null; + public Task AAsync(double value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericFloatingPointToInteger_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(double value) => throw null; + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(1.0); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericInt64ToFloatingPoint_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(long value) => throw null; + public Task AAsync(double value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(1L); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericInt32ToFloatingPoint_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(int value) => throw null; + public Task AAsync(float value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(1); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericByteToInt32_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(byte value) => throw null; + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A((byte)1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericInt16ToInt32_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(short value) => throw null; + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A((short)1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericSingleToDouble_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(float value) => throw null; + public Task AAsync(double value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(1f)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericHalfToSingle_ShouldReport() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net6_0) + .WithSourceCode(""" + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(Half value) => throw null; + public Task AAsync(float value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A((Half)1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitNumericHalfToDouble_ShouldReport() + { + await CreateProjectBuilder() + .WithTargetFramework(TargetFramework.Net6_0) + .WithSourceCode(""" + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(Half value) => throw null; + public Task AAsync(double value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A((Half)1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_CompatibleGenericDefinitions_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(List value) => throw null; + public Task AAsync(IReadOnlyCollection value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(new List())|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_DifferentArity_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(Dictionary value) => throw null; + public Task AAsync(List value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(new Dictionary()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_ConstraintNewIncompatible_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + class WithoutPublicParameterlessConstructor + { + public WithoutPublicParameterlessConstructor(int value) + { + } + } + + class Test + { + public void A(List value) => throw null; + public Task AAsync(List value, CancellationToken token = default) + where T : new() => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(new List()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericArgument_DifferentTypeConstraintsOrder_ShouldNotReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + interface IMark1 { } + interface IMark2 { } + + class Mark : IMark1, IMark2 { } + + class Test + { + public void A(int i, List test) where T : IMark1, IMark2 => throw null; + public Task AAsync(int i, List test, CancellationToken token = default) where T : IMark2, IMark1 => throw null; + } + + class Demo + { + public async Task M() + { + new Test().A(1, new List()); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task GenericMethod_SameConstraints_Diagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + + interface IMark1 { } + interface IMark2 { } + + class Mark : IMark1, IMark2 { } + + class Test + { + public void A(int i, List test) where T : class, IMark1, IMark2 => throw null; + public Task AAsync(int i, List test, CancellationToken token = default) where T : class, IMark1, IMark2 => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(1, new List())|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_ImplicitUserDefinedConversion_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Value + { + public static implicit operator Value(string value) => new Value(); + } + + class Test + { + public void A(Value value) => throw null; + public Task AAsync(Value value, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A("value")|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task ExtensionMethodToInstanceMethod_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public Task AAsync(int value, CancellationToken token = default) => throw null; + } + + static class TestExtensions + { + public static void A(this Test test, int value) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(1)|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task Argument_NamedArgumentsReordered_ShouldReport() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + + class Test + { + public void A(int left, int right) => throw null; + public Task AAsync(int left, int right, CancellationToken token = default) => throw null; + } + + class Demo + { + public async Task M() + { + [|new Test().A(right: 2, left: 1)|]; } } """)