Skip to content
Merged
Show file tree
Hide file tree
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
72 changes: 55 additions & 17 deletions src/TUnit.Analyzers/TestDataAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -417,25 +417,29 @@ private void CheckMethodDataSource(SymbolAnalysisContext context,
.FirstOrDefault(x => x.Key == "Arguments")
.Value;

var argumentForMethodCallTypes =
// Kept as TypedConstants (not just their types) so a null element can be told apart: its Type is the
// array's element type (object), which would otherwise fail to convert to every parameter type.
var argumentsForMethodCall =
argumentsNamedArgument.Kind == TypedConstantKind.Array
? argumentsNamedArgument
.Values
.Select(x => x.Type)
.OfType<ITypeSymbol>()
.ToArray()
: Array.Empty<ITypeSymbol>();
? argumentsNamedArgument.Values.ToArray()
: Array.Empty<TypedConstant>();

// Look for methods first
var methodSymbols = (type as INamedTypeSymbol)?.GetSelfAndBaseTypes()
.SelectMany(x => x.GetMembers())
.OfType<IMethodSymbol>()
.ToArray() ?? Array.Empty<IMethodSymbol>();

// An overload that takes exactly the Arguments wins over one that also takes an injected trailing
// CancellationToken, as it does at runtime, whatever order the overloads are declared in.
var dataSourceMethod = methodSymbols
.FirstOrDefault(methodSymbol =>
methodSymbol.Name == methodName &&
MatchesParameters(context, argumentForMethodCallTypes, methodSymbol))
ArgumentsMatchParameterTypes(context, argumentsForMethodCall, methodSymbol.Parameters.Select(p => p.Type)))
?? methodSymbols
.FirstOrDefault(methodSymbol =>
methodSymbol.Name == methodName &&
ArgumentsMatchParameterTypes(context, argumentsForMethodCall, methodSymbol.Parameters.WithoutCancellationTokenParameter().Select(p => p.Type)))
?? methodSymbols.FirstOrDefault(x => x.Name == methodName);

// If no method found, check for properties or fields
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand Down Expand Up @@ -634,16 +638,13 @@ private void CheckMethodDataSource(SymbolAnalysisContext context,
return;
}

var argumentsToParamsConversions = argumentForMethodCallTypes.ZipAll(dataSourceMethodParameterTypes,
(argument, parameterType) => context.Compilation.HasImplicitConversionOrGenericParameter(argument, parameterType));

if (!argumentsToParamsConversions.All(x => x))
if (!ArgumentsMatchParameterTypes(context, argumentsForMethodCall, dataSourceMethodParameterTypes))
{
context.ReportDiagnostic(
Diagnostic.Create(
Rules.WrongArgumentTypeTestData,
attribute.GetLocation(),
string.Join<ITypeSymbol>(", ", argumentForMethodCallTypes),
string.Join(", ", argumentsForMethodCall.Select(x => x.IsNull ? "null" : x.Type?.ToString())),
string.Join<ITypeSymbol>(", ", dataSourceMethodParameterTypes),
"for the `Arguments` array")
);
Expand Down Expand Up @@ -713,11 +714,48 @@ private void CheckMethodDataSource(SymbolAnalysisContext context,
}
}

private static bool MatchesParameters(SymbolAnalysisContext context, ITypeSymbol[] argumentForMethodCallTypes, IMethodSymbol methodSymbol)
private static bool ArgumentsMatchParameterTypes(SymbolAnalysisContext context, TypedConstant[] argumentsForMethodCall,
IEnumerable<ITypeSymbol> parameterTypes)
{
return argumentForMethodCallTypes.ZipAll(methodSymbol.Parameters.Select(p => p.Type),
(argument, parameter) => context.Compilation.HasImplicitConversionOrGenericParameter(argument, parameter))
.All(x => x);
var parameterTypeArray = parameterTypes as ITypeSymbol[] ?? parameterTypes.ToArray();

// Checked up front: a missing argument would otherwise reach the comparison below as default(TypedConstant),
// whose IsNull is true, and pass for any parameter that accepts null.
if (argumentsForMethodCall.Length != parameterTypeArray.Length)
{
return false;
}

for (var i = 0; i < argumentsForMethodCall.Length; i++)
{
var argument = argumentsForMethodCall[i];
var matches = argument.IsNull
? CanAcceptNull(parameterTypeArray[i])
: context.Compilation.HasImplicitConversionOrGenericParameter(argument.Type, parameterTypeArray[i]);

if (!matches)
{
return false;
}
}

return true;
}

// Mirrors the null-argument overload selection in both execution modes (added in #7143): a null argument binds to
// any parameter that can hold null, i.e. anything other than a non-nullable value type.
private static bool CanAcceptNull(ITypeSymbol parameterType)
{
if (parameterType is ITypeParameterSymbol typeParameter)
{
// Neither mode infers a generic data source method's type arguments from a null argument.
return typeParameter.TypeParameterKind != TypeParameterKind.Method
&& !typeParameter.HasValueTypeConstraint
&& !typeParameter.HasUnmanagedTypeConstraint;
}

return !parameterType.IsValueType
|| parameterType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
}

private ImmutableArray<ITypeSymbol> UnwrapTypes(SymbolAnalysisContext context,
Expand Down
203 changes: 203 additions & 0 deletions tests/TUnit.Analyzers.Tests/MethodDataSourceAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,209 @@ public static Func<int> Data(bool flag)
);
}

[Test]
public async Task Null_Argument_Is_Not_Flagged_For_Reference_Type_Parameter()
{
await Verifier
.VerifyAnalyzerAsync(
"""
#nullable enable
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[MethodDataSource(nameof(Data), Arguments = new object?[] { null })]
[Test]
public void MyTest(string value)
{
}

public static IEnumerable<string> Data(string? value)
{
yield return value ?? "null";
}
}
"""
);
}

[Test]
public async Task Null_Argument_Is_Not_Flagged_For_Nullable_Value_Type_Parameter()
{
await Verifier
.VerifyAnalyzerAsync(
"""
#nullable enable
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[MethodDataSource(nameof(Data), Arguments = new object?[] { null })]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<int> Data(int? value)
{
yield return value ?? 0;
}
}
"""
);
}

[Test]
public async Task Null_Argument_Selects_Overload_That_Accepts_Null()
{
await Verifier
.VerifyAnalyzerAsync(
"""
#nullable enable
using System.Collections.Generic;
using System.Threading.Tasks;
using TUnit.Core;

public class MyClass
{
public static IEnumerable<int> NullRows(int value) { yield return value; }
public static IEnumerable<string> NullRows(string? value) { yield return value ?? "null"; }

[Test]
[MethodDataSource(nameof(NullRows), Arguments = new object?[] { null })]
public Task Null_Argument<T>(T value) => Task.CompletedTask;
}
"""
);
}

[Test]
public async Task Null_Argument_Is_Flagged_For_Non_Nullable_Value_Type_Parameter()
{
await Verifier
.VerifyAnalyzerAsync(
"""
#nullable enable
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[{|#0:MethodDataSource(nameof(Data), Arguments = new object?[] { null })|}]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<int> Data(int value)
{
yield return value;
}
}
""",

Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("null", "int")
);
}

[Test]
public async Task Missing_Argument_Is_Flagged_For_Parameter_That_Accepts_Null()
{
await Verifier
.VerifyAnalyzerAsync(
"""
#nullable enable
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[{|#0:MethodDataSource(nameof(Data), Arguments = new object[] { 1 })|}]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<int> Data(int value, string? label)
{
yield return value;
}
}
""",

Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("int", "int, string?")
);
}

[Test]
public async Task Exact_Overload_Wins_Over_Earlier_Overload_With_CancellationToken()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Collections.Generic;
using System.Threading;
using TUnit.Core;

public class MyClass
{
[MethodDataSource(nameof(Data), Arguments = new object[] { 1 })]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<string> Data(int value, CancellationToken cancellationToken)
{
yield return "token";
}

public static IEnumerable<int> Data(int value)
{
yield return value;
}
}
"""
);
}

[Test]
public async Task Null_Argument_Is_Flagged_For_Method_Type_Parameter()
{
await Verifier
.VerifyAnalyzerAsync(
"""
#nullable enable
using System.Collections.Generic;
using TUnit.Core;

public class MyClass
{
[{|#0:MethodDataSource(nameof(Data), Arguments = new object?[] { null })|}]
[Test]
public void MyTest(int value)
{
}

public static IEnumerable<int> Data<T>(T value) where T : notnull
{
yield return 1;
}
}
""",

Verifier.Diagnostic(Rules.WrongArgumentTypeTestData)
.WithLocation(0)
.WithArguments("null", "T")
);
}

[Test]
public async Task Arguments_Are_Not_Flagged_When_Argument_Is_Convertible()
{
Expand Down
Loading