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
71 changes: 71 additions & 0 deletions src/TUnit.Analyzers/Extensions/CompilationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ public static bool HasImplicitConversionOrGenericParameter(this Compilation comp
return true;
}

if (argumentType is not null && parameterType is not null
&& ContainsArray(parameterType) && ContainsTypeParameter(parameterType))
{
// Constraint and general inference validation remain separate (#7520).
return compilation.MatchesOpenArrayShape(argumentType, parameterType);
}

if (parameterType?.IsGenericDefinition() == false)
{
if (argumentType is null)
Expand Down Expand Up @@ -49,6 +56,70 @@ public static bool HasImplicitConversionOrGenericParameter(this Compilation comp
return compilation.HasImplicitConversion(argumentType?.OriginalDefinition, parameterType?.OriginalDefinition);
}

// Nested generic arguments deliberately retain the existing inference behavior (#7520).
private static bool ContainsArray(ITypeSymbol type) => type switch
{
IArrayTypeSymbol => true,
INamedTypeSymbol { IsTupleType: true } tuple => tuple.TupleElements.Any(x => ContainsArray(x.Type)),
_ => false
};

private static bool MatchesOpenArrayShape(this Compilation compilation, ITypeSymbol argumentType,
ITypeSymbol parameterType, bool requiresIdentity = false)
{
if (ContainsTypeParameter(parameterType))
{
var conversion = compilation.ClassifyConversion(argumentType, parameterType);
if (conversion.IsIdentity || (!requiresIdentity && conversion.IsImplicit))
{
return true;
}

if (parameterType is IArrayTypeSymbol targetArray)
{
if (argumentType is not IArrayTypeSymbol sourceArray
|| sourceArray.Rank != targetArray.Rank || sourceArray.IsSZArray != targetArray.IsSZArray)
{
return false;
}

return compilation.MatchesOpenArrayShape(sourceArray.ElementType, targetArray.ElementType, requiresIdentity: true);
}

if (parameterType is INamedTypeSymbol { IsTupleType: true } targetTuple)
{
if (argumentType is not INamedTypeSymbol { IsTupleType: true } sourceTuple
|| sourceTuple.TupleElements.Length != targetTuple.TupleElements.Length)
{
return false;
}

for (var i = 0; i < sourceTuple.TupleElements.Length; i++)
{
if (!compilation.MatchesOpenArrayShape(sourceTuple.TupleElements[i].Type,
targetTuple.TupleElements[i].Type, requiresIdentity))
{
return false;
}
}

return true;
}

// Inferring nested generic arguments, constraints and shared bounds is deliberately out of scope.
return true;
}

// Value-type array elements (including tuples) are invariant. Their fields cannot
// use the numeric or reference conversions available to a standalone tuple.
if (requiresIdentity)
{
return compilation.ClassifyConversion(argumentType, parameterType).IsIdentity;
}

return compilation.HasImplicitConversionOrGenericParameter(argumentType, parameterType);
}

/// <summary>
/// Whether a data source value of <paramref name="argumentType"/> can bind to a test parameter of
/// <paramref name="parameterType"/>. A bare type-parameter target accepts any argument type, because the
Expand Down
107 changes: 107 additions & 0 deletions tests/TUnit.Analyzers.Tests/GenericTupleArrayMethodDataSourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using TUnit.Analyzers.Extensions;
using Assert = TUnit.Assertions.Assert;
using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.Analyzers.TestDataAnalyzer>;

namespace TUnit.Analyzers.Tests;

public class GenericTupleArrayMethodDataSourceTests : BaseAnalyzerTests
{
[Test]
public async Task RankOneNonSzArrayDoesNotBindToOpenVector()
{
// C# cannot spell T[*]. Import a field with that signature from metadata.
var metadata = new MetadataBuilder();
metadata.AddModule(0, metadata.GetOrAddString("ArrayShapes"), metadata.GetOrAddGuid(Guid.NewGuid()), default, default);
metadata.AddAssembly(metadata.GetOrAddString("ArrayShapes"), new Version(1, 0), default, default, default, default);
metadata.AddTypeDefinition(TypeAttributes.NotPublic, default, metadata.GetOrAddString("<Module>"),
default, MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1));
var type = metadata.AddTypeDefinition(TypeAttributes.Public, default, metadata.GetOrAddString("Shapes`1"),
default, MetadataTokens.FieldDefinitionHandle(1), MetadataTokens.MethodDefinitionHandle(1));
metadata.AddGenericParameter(type, GenericParameterAttributes.None, metadata.GetOrAddString("T"), 0);
// FIELD, ARRAY, VAR 0, rank 1, no sizes, no lower bounds.
metadata.AddFieldDefinition(FieldAttributes.Public, metadata.GetOrAddString("NonVector"),
metadata.GetOrAddBlob(new byte[] { 0x06, 0x14, 0x13, 0x00, 0x01, 0x00, 0x00 }));
var pe = new ManagedPEBuilder(new PEHeaderBuilder(), new MetadataRootBuilder(metadata), new BlobBuilder());
var image = new BlobBuilder();
pe.Serialize(image);
var compilation = CSharpCompilation.Create("Consumer",
references: [MetadataReference.CreateFromImage(image.ToImmutableArray())]);
var shapes = compilation.GetTypeByMetadataName("Shapes`1")!;
var nonVector = (IArrayTypeSymbol)((IFieldSymbol)shapes.GetMembers("NonVector").Single()).Type;
var vector = compilation.CreateArrayTypeSymbol(shapes.TypeParameters[0]);

await Assert.That(nonVector.Rank).IsEqualTo(1);
await Assert.That(nonVector.IsSZArray).IsFalse();
await Assert.That(compilation.HasImplicitConversionOrGenericParameter(nonVector, vector)).IsFalse();
await Assert.That(compilation.HasImplicitConversionOrGenericParameter(vector, nonVector)).IsFalse();
}

[Test]
[Arguments("(int, int)[]", "(T, long)[]", true)]
[Arguments("(int, string[])[]", "(T, object[])[]", true)]
[Arguments("(int, int)[]", "(T, int)[]", false)]
[Arguments("(int, dynamic)[]", "(T, object)[]", false)]
[Arguments("(int, object)[]", "(T, dynamic)[]", false)]
[Arguments("(int, string)[]", "(T, object)[]", true)]
[Arguments("(int, (int, dynamic)[])", "(T, (T, object)[])", false)]
[Arguments("int[]", "T[]", false)]
[Arguments("int[][]", "T[][]", false)]
[Arguments("string[][]", "T[][]", false)]
[Arguments("int", "(T, int[])", true)]
[Arguments("int[]", "(T, int[])", true)]
[Arguments("int[]", "(T, int[])[]", true)]
[Arguments("(int, int, int[])", "(T, int[])", true)]
[Arguments("int[,]", "T[,]", false)]
[Arguments("int[]", "T[,]", true)]
[Arguments("int", "T[]", true)]
[Arguments("int[]", "string[]", true)]
[Arguments("(int, int[])", "(T, T[])", false)]
// Shared-bound inference remains permissive until #7520.
[Arguments("(int, string[])", "(T, T[])", false)]
[Arguments("(int, (int, int[][]))", "(T, (T, T[][]))", false)]
[Arguments("(int, string[])", "(T, int[])", true)]
[Arguments("List<int>[]", "IEnumerable<T>[]", false)]
[Arguments("IConsumer<string, object>[]", "IConsumer<T, string>[]", false)]
[Arguments("(int, string[])", "(T, object[])", false)]
public async Task TupleArrayElements(string sourceElement, string targetElement, bool expectDiagnostic)
{
var attribute = expectDiagnostic ? "{|#0:MethodDataSource(nameof(Row))|}" : "MethodDataSource(nameof(Row))";
var source = $$"""
#pragma warning disable TUnit0046 // Mutable array rows are intentional in these binding tests.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TUnit.Core;
using TUnit.Core.Interfaces;
public interface IConsumer<A, in B> { }
public class C
{
public static (Fixture, CancellationToken, {{sourceElement}}) Row() => default;
[Test, {{attribute}}]
public void Test<T>((Fixture, CancellationToken, {{targetElement}}) row) { }
public struct Fixture : IAsyncInitializer
{
public Task InitializeAsync() => Task.CompletedTask;
}
}
""";

if (expectDiagnostic)
{
await Verifier.VerifyAnalyzerAsync(source,
Verifier.Diagnostic(Rules.WrongArgumentTypeTestData).WithLocation(0)
.WithArguments($"Fixture, CancellationToken, {sourceElement}", $"(Fixture, CancellationToken, {targetElement})"));
}
else
{
await Verifier.VerifyAnalyzerAsync(source);
}
}
}
Loading